/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.ui.internal.commands;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.util.Util;
import org.eclipse.ui.keys.KeySequence;
import org.eclipse.ui.keys.KeyStroke;

final class KeySequenceBindingNode {

	final static class Assignment implements Comparable {
		boolean hasPluginCommandIdInFirstKeyConfiguration;
		boolean hasPluginCommandIdInInheritedKeyConfiguration;

		boolean hasPreferenceCommandIdInFirstKeyConfiguration;
		boolean hasPreferenceCommandIdInInheritedKeyConfiguration;
		String pluginCommandIdInFirstKeyConfiguration;
		String pluginCommandIdInInheritedKeyConfiguration;
		String preferenceCommandIdInFirstKeyConfiguration;
		String preferenceCommandIdInInheritedKeyConfiguration;

		public int compareTo(Object object) {
			Assignment castedObject = (Assignment) object;
			int compareTo =
				hasPreferenceCommandIdInFirstKeyConfiguration == false
					? (castedObject.hasPreferenceCommandIdInFirstKeyConfiguration
						== true
						? -1
						: 0)
					: 1;

			if (compareTo == 0) {
				compareTo =
					hasPreferenceCommandIdInInheritedKeyConfiguration == false
						? (castedObject
							.hasPreferenceCommandIdInInheritedKeyConfiguration
							== true
							? -1
							: 0)
						: 1;

				if (compareTo == 0) {
					compareTo =
						hasPluginCommandIdInFirstKeyConfiguration == false
							? (castedObject
								.hasPluginCommandIdInFirstKeyConfiguration
								== true
								? -1
								: 0)
							: 1;

					if (compareTo == 0) {
						compareTo =
							hasPluginCommandIdInInheritedKeyConfiguration
								== false
								? (castedObject
									.hasPluginCommandIdInInheritedKeyConfiguration
									== true
									? -1
									: 0)
								: 1;

						if (compareTo == 0) {
							compareTo =
								Util.compare(
									preferenceCommandIdInFirstKeyConfiguration,
									castedObject
										.preferenceCommandIdInFirstKeyConfiguration);

							if (compareTo == 0) {
								compareTo =
									Util.compare(
										preferenceCommandIdInInheritedKeyConfiguration,
										castedObject
											.preferenceCommandIdInInheritedKeyConfiguration);

								if (compareTo == 0) {
									compareTo =
										Util.compare(
											pluginCommandIdInFirstKeyConfiguration,
											castedObject
												.pluginCommandIdInFirstKeyConfiguration);

									if (compareTo == 0)
										compareTo =
											Util.compare(
												pluginCommandIdInInheritedKeyConfiguration,
												castedObject
													.pluginCommandIdInInheritedKeyConfiguration);
								}
							}
						}
					}
				}
			}

			return compareTo;
		}

		boolean contains(String commandId) {
			return Util.equals(
				commandId,
				preferenceCommandIdInFirstKeyConfiguration)
				|| Util.equals(
					commandId,
					preferenceCommandIdInInheritedKeyConfiguration)
				|| Util.equals(commandId, pluginCommandIdInFirstKeyConfiguration)
				|| Util.equals(
					commandId,
					pluginCommandIdInInheritedKeyConfiguration);
		}

		public boolean equals(Object object) {
			if (!(object instanceof Assignment))
				return false;

			Assignment castedObject = (Assignment) object;
			boolean equals = true;
			equals &= hasPreferenceCommandIdInFirstKeyConfiguration
				== castedObject.hasPreferenceCommandIdInFirstKeyConfiguration;
			equals &= hasPreferenceCommandIdInInheritedKeyConfiguration
				== castedObject.hasPreferenceCommandIdInInheritedKeyConfiguration;
			equals &= hasPluginCommandIdInFirstKeyConfiguration
				== castedObject.hasPluginCommandIdInFirstKeyConfiguration;
			equals &= hasPluginCommandIdInInheritedKeyConfiguration
				== castedObject.hasPluginCommandIdInInheritedKeyConfiguration;
			equals &= preferenceCommandIdInFirstKeyConfiguration
				== castedObject.preferenceCommandIdInFirstKeyConfiguration;
			equals &= preferenceCommandIdInInheritedKeyConfiguration
				== castedObject.preferenceCommandIdInInheritedKeyConfiguration;
			equals &= pluginCommandIdInFirstKeyConfiguration
				== castedObject.pluginCommandIdInFirstKeyConfiguration;
			equals &= pluginCommandIdInInheritedKeyConfiguration
				== castedObject.pluginCommandIdInInheritedKeyConfiguration;
			return equals;
		}
	}

	static void add(
		Map keyStrokeNodeByKeyStrokeMap,
		KeySequence keySequence,
		String contextId,
		String keyConfigurationId,
		int rank,
		String platform,
		String locale,
		String commandId) {
		List keyStrokes = keySequence.getKeyStrokes();
		Map root = keyStrokeNodeByKeyStrokeMap;
		KeySequenceBindingNode keySequenceBindingNode = null;

		for (int i = 0; i < keyStrokes.size(); i++) {
			KeyStroke keyStroke = (KeyStroke) keyStrokes.get(i);
			keySequenceBindingNode =
				(KeySequenceBindingNode) root.get(keyStroke);

			if (keySequenceBindingNode == null) {
				keySequenceBindingNode = new KeySequenceBindingNode();
				root.put(keyStroke, keySequenceBindingNode);
			}

			root = keySequenceBindingNode.childKeyStrokeNodeByKeyStrokeMap;
		}

		if (keySequenceBindingNode != null)
			keySequenceBindingNode.add(
				contextId,
				keyConfigurationId,
				rank,
				platform,
				locale,
				commandId);
	}

	static Map find(Map keyStrokeNodeByKeyStrokeMap, KeySequence keySequence) {
		Iterator iterator = keySequence.getKeyStrokes().iterator();
		KeySequenceBindingNode keySequenceBindingNode = null;

		while (iterator.hasNext()) {
			keySequenceBindingNode =
				(KeySequenceBindingNode) keyStrokeNodeByKeyStrokeMap.get(
					iterator.next());

			if (keySequenceBindingNode == null)
				return null;

			keyStrokeNodeByKeyStrokeMap =
				keySequenceBindingNode.childKeyStrokeNodeByKeyStrokeMap;
		}

		return keyStrokeNodeByKeyStrokeMap;
	}

	static Map getAssignmentsByContextIdKeySequence(
		Map keyStrokeNodeByKeyStrokeMap,
		KeySequence prefix) {
		Map assignmentsByContextIdByKeySequence = new HashMap();
		Iterator iterator = keyStrokeNodeByKeyStrokeMap.entrySet().iterator();

		while (iterator.hasNext()) {
			Map.Entry entry = (Map.Entry) iterator.next();
			KeyStroke keyStroke = (KeyStroke) entry.getKey();
			KeySequenceBindingNode keySequenceBindingNode =
				(KeySequenceBindingNode) entry.getValue();
			List keyStrokes = new ArrayList(prefix.getKeyStrokes());
			keyStrokes.add(keyStroke);
			KeySequence keySequence = KeySequence.getInstance(keyStrokes);
			Map childAssignmentsByContextIdByKeySequence =
				getAssignmentsByContextIdKeySequence(
					keySequenceBindingNode.childKeyStrokeNodeByKeyStrokeMap,
					keySequence);

			if (childAssignmentsByContextIdByKeySequence.size() >= 1)
				assignmentsByContextIdByKeySequence.putAll(
					childAssignmentsByContextIdByKeySequence);

			assignmentsByContextIdByKeySequence.put(
				keySequence,
				keySequenceBindingNode.assignmentsByContextId);
		}

		return assignmentsByContextIdByKeySequence;
	}

	static void getKeySequenceBindingDefinitions(
		Map keyStrokeNodeByKeyStrokeMap,
		KeySequence prefix,
		int rank,
		List keySequenceBindingDefinitions) {
		Iterator iterator = keyStrokeNodeByKeyStrokeMap.entrySet().iterator();

		while (iterator.hasNext()) {
			Map.Entry entry = (Map.Entry) iterator.next();
			KeyStroke keyStroke = (KeyStroke) entry.getKey();
			KeySequenceBindingNode keySequenceBindingNode =
				(KeySequenceBindingNode) entry.getValue();
			List keyStrokes = new ArrayList(prefix.getKeyStrokes());
			keyStrokes.add(keyStroke);
			KeySequence keySequence = KeySequence.getInstance(keyStrokes);
			Map contextMap = keySequenceBindingNode.contextMap;
			Iterator iterator2 = contextMap.entrySet().iterator();

			while (iterator2.hasNext()) {
				Map.Entry entry2 = (Map.Entry) iterator2.next();
				String contextId = (String) entry2.getKey();
				Map keyConfigurationMap = (Map) entry2.getValue();
				Iterator iterator3 = keyConfigurationMap.entrySet().iterator();

				while (iterator3.hasNext()) {
					Map.Entry entry3 = (Map.Entry) iterator3.next();
					String keyConfigurationId = (String) entry3.getKey();
					Map rankMap = (Map) entry3.getValue();
					Map platformMap = (Map) rankMap.get(new Integer(rank));

					if (platformMap != null) {
						Iterator iterator4 = platformMap.entrySet().iterator();

						while (iterator4.hasNext()) {
							Map.Entry entry4 = (Map.Entry) iterator4.next();
							String platform = (String) entry4.getKey();
							Map localeMap = (Map) entry4.getValue();
							Iterator iterator5 =
								localeMap.entrySet().iterator();

							while (iterator5.hasNext()) {
								Map.Entry entry5 = (Map.Entry) iterator5.next();
								String locale = (String) entry5.getKey();
								Set commandIds = (Set) entry5.getValue();
								Iterator iterator6 = commandIds.iterator();

								while (iterator6.hasNext()) {
									String commandId =
										(String) iterator6.next();
									keySequenceBindingDefinitions.add(
										new KeySequenceBindingDefinition(
											contextId,
											commandId,
											keyConfigurationId,
											keySequence,
											locale,
											platform,
											null));
								}
							}
						}
					}
				}
			}

			getKeySequenceBindingDefinitions(
				keySequenceBindingNode.childKeyStrokeNodeByKeyStrokeMap,
				keySequence,
				rank,
				keySequenceBindingDefinitions);
		}
	}

	static Map getKeySequenceBindingsByCommandId(Map keySequenceMap) {
		Map commandMap = new HashMap();
		Iterator iterator = keySequenceMap.entrySet().iterator();

		while (iterator.hasNext()) {
			Map.Entry entry = (Map.Entry) iterator.next();
			KeySequence keySequence = (KeySequence) entry.getKey();
			Match match = (Match) entry.getValue();
			String commandId = match.getCommandId();
			int value = match.getValue();
			SortedSet keySequenceBindings =
				(SortedSet) commandMap.get(commandId);

			if (keySequenceBindings == null) {
				keySequenceBindings = new TreeSet();
				commandMap.put(commandId, keySequenceBindings);
			}

			keySequenceBindings.add(new KeySequenceBinding(keySequence, value));
		}

		return commandMap;
	}

	static Map getMatchesByKeySequence(
		Map keyStrokeNodeByKeyStrokeMap,
		KeySequence prefix) {
		Map keySequenceMap = new HashMap();
		Iterator iterator = keyStrokeNodeByKeyStrokeMap.entrySet().iterator();

		while (iterator.hasNext()) {
			Map.Entry entry = (Map.Entry) iterator.next();
			KeyStroke keyStroke = (KeyStroke) entry.getKey();
			KeySequenceBindingNode keySequenceBindingNode =
				(KeySequenceBindingNode) entry.getValue();
			List keyStrokes = new ArrayList(prefix.getKeyStrokes());
			keyStrokes.add(keyStroke);
			KeySequence keySequence = KeySequence.getInstance(keyStrokes);
			Map childMatchesByKeySequence =
				getMatchesByKeySequence(
					keySequenceBindingNode.childKeyStrokeNodeByKeyStrokeMap,
					keySequence);

			if (childMatchesByKeySequence.size() >= 1)
				keySequenceMap.putAll(childMatchesByKeySequence);
			else if (
				keySequenceBindingNode.match != null
					&& keySequenceBindingNode.match.getCommandId() != null)
				keySequenceMap.put(keySequence, keySequenceBindingNode.match);
		}

		return keySequenceMap;
	}

	static void remove(
		Map keyStrokeNodeByKeyStrokeMap,
		KeySequence keySequence,
		String contextId,
		String keyConfigurationId,
		int rank,
		String platform,
		String locale) {
		Iterator iterator = keySequence.getKeyStrokes().iterator();
		KeySequenceBindingNode keySequenceBindingNode = null;

		while (iterator.hasNext()) {
			keySequenceBindingNode =
				(KeySequenceBindingNode) keyStrokeNodeByKeyStrokeMap.get(
					iterator.next());

			if (keySequenceBindingNode == null)
				return;

			keyStrokeNodeByKeyStrokeMap =
				keySequenceBindingNode.childKeyStrokeNodeByKeyStrokeMap;
		}

		keySequenceBindingNode.remove(
			contextId,
			keyConfigurationId,
			rank,
			platform,
			locale);
	}

	static void remove(
		Map keyStrokeNodeByKeyStrokeMap,
		KeySequence keySequence,
		String contextId,
		String keyConfigurationId,
		int rank,
		String platform,
		String locale,
		String commandId) {
		Iterator iterator = keySequence.getKeyStrokes().iterator();
		KeySequenceBindingNode keySequenceBindingNode = null;

		while (iterator.hasNext()) {
			keySequenceBindingNode =
				(KeySequenceBindingNode) keyStrokeNodeByKeyStrokeMap.get(
					iterator.next());

			if (keySequenceBindingNode == null)
				return;

			keyStrokeNodeByKeyStrokeMap =
				keySequenceBindingNode.childKeyStrokeNodeByKeyStrokeMap;
		}

		keySequenceBindingNode.remove(
			contextId,
			keyConfigurationId,
			rank,
			platform,
			locale,
			commandId);
	}

	static void solve(
		Map keyStrokeNodeByKeyStrokeMap,
		String[] keyConfigurationIds,
		String[] platforms,
		String[] locales) {
		for (Iterator iterator =
			keyStrokeNodeByKeyStrokeMap.values().iterator();
			iterator.hasNext();
			) {
			KeySequenceBindingNode keySequenceBindingNode =
				(KeySequenceBindingNode) iterator.next();
			keySequenceBindingNode.solveAssignmentsByContextId(
				keyConfigurationIds,
				platforms,
				locales);
			solve(
				keySequenceBindingNode.childKeyStrokeNodeByKeyStrokeMap,
				keyConfigurationIds,
				platforms,
				locales);
		}
	}

	static void solve(
		Map keyStrokeNodeByKeyStrokeMap,
		Map contextIds,
		String[] keyConfigurationIds,
		String[] platforms,
		String[] locales) {
		for (Iterator iterator =
			keyStrokeNodeByKeyStrokeMap.values().iterator();
			iterator.hasNext();
			) {
			KeySequenceBindingNode keySequenceBindingNode =
			    (KeySequenceBindingNode) iterator.next();
			keySequenceBindingNode.solveMatch(
				contextIds,
				keyConfigurationIds,
				platforms,
				locales);
			solve(
				keySequenceBindingNode.childKeyStrokeNodeByKeyStrokeMap,
				contextIds,
				keyConfigurationIds,
				platforms,
				locales);
		}
	}
	private Map contextMap = new HashMap();

	private Map assignmentsByContextId = new HashMap();
	private Map childKeyStrokeNodeByKeyStrokeMap = new HashMap();
	private Match match = null;

	private KeySequenceBindingNode() {
	}

	private void add(
		String contextId,
		String keyConfigurationId,
		int rank,
		String platform,
		String locale,
		String commandId) {
		Map keyConfigurationMap = (Map) contextMap.get(contextId);

		if (keyConfigurationMap == null) {
			keyConfigurationMap = new HashMap();
			contextMap.put(contextId, keyConfigurationMap);
		}

		Map rankMap = (Map) keyConfigurationMap.get(keyConfigurationId);

		if (rankMap == null) {
			rankMap = new HashMap();
			keyConfigurationMap.put(keyConfigurationId, rankMap);
		}

		Map platformMap = (Map) rankMap.get(new Integer(rank));

		if (platformMap == null) {
			platformMap = new HashMap();
			rankMap.put(new Integer(rank), platformMap);
		}

		Map localeMap = (Map) platformMap.get(platform);

		if (localeMap == null) {
			localeMap = new HashMap();
			platformMap.put(platform, localeMap);
		}

		Set commandIds = (Set) localeMap.get(locale);

		if (commandIds == null) {
			commandIds = new HashSet();
			localeMap.put(locale, commandIds);
		}

		commandIds.add(commandId);
	}

	private void remove(
		String contextId,
		String keyConfigurationId,
		int rank,
		String platform,
		String locale) {
		Map keyConfigurationMap = (Map) contextMap.get(contextId);

		if (keyConfigurationMap != null) {
			Map rankMap = (Map) keyConfigurationMap.get(keyConfigurationId);

			if (rankMap != null) {
				Map platformMap = (Map) rankMap.get(new Integer(rank));

				if (platformMap != null) {
					Map localeMap = (Map) platformMap.get(platform);

					if (localeMap != null) {
						localeMap.remove(locale);

						if (localeMap.isEmpty()) {
							platformMap.remove(platform);

							if (platformMap.isEmpty()) {
								rankMap.remove(new Integer(rank));

								if (rankMap.isEmpty()) {
									keyConfigurationMap.remove(
										keyConfigurationId);

									if (keyConfigurationMap.isEmpty())
										contextMap.remove(contextId);
								}
							}
						}
					}
				}
			}
		}
	}

	private void remove(
		String contextId,
		String keyConfigurationId,
		int rank,
		String platform,
		String locale,
		String commandId) {
		Map keyConfigurationMap = (Map) contextMap.get(contextId);

		if (keyConfigurationMap != null) {
			Map rankMap = (Map) keyConfigurationMap.get(keyConfigurationId);

			if (rankMap != null) {
				Map platformMap = (Map) rankMap.get(new Integer(rank));

				if (platformMap != null) {
					Map localeMap = (Map) platformMap.get(platform);

					if (localeMap != null) {
						Set commandIds = (Set) localeMap.get(locale);

						if (commandIds != null) {
							commandIds.remove(commandId);

							if (commandIds.isEmpty()) {
								localeMap.remove(locale);

								if (localeMap.isEmpty()) {
									platformMap.remove(platform);

									if (platformMap.isEmpty()) {
										rankMap.remove(new Integer(rank));

										if (rankMap.isEmpty()) {
											keyConfigurationMap.remove(
												keyConfigurationId);

											if (keyConfigurationMap.isEmpty())
												contextMap.remove(contextId);
										}
									}
								}
							}
						}
					}
				}
			}
		}
	}

	private void solveAssignmentsByContextId(
		String[] keyConfigurationIds,
		String[] platforms,
		String[] locales) {
		assignmentsByContextId.clear();

		for (Iterator iterator = contextMap.entrySet().iterator();
			iterator.hasNext();
			) {
			Map.Entry entry = (Map.Entry) iterator.next();
			String contextId = (String) entry.getKey();
			Map keyConfigurationMap = (Map) entry.getValue();
			KeySequenceBindingNode.Assignment assignment = null;

			if (keyConfigurationMap != null)
				for (int keyConfiguration = 0;
					keyConfiguration < keyConfigurationIds.length
						&& keyConfiguration < 0xFF;
					keyConfiguration++) {
					Map rankMap =
						(Map) keyConfigurationMap.get(
							keyConfigurationIds[keyConfiguration]);

					if (rankMap != null)
						for (int rank = 0; rank <= 1; rank++) {
							Map platformMap =
								(Map) rankMap.get(new Integer(rank));

							if (platformMap != null)
								for (int platform = 0;
									platform < platforms.length
										&& platform < 0xFF;
									platform++) {
									Map localeMap =
										(Map) platformMap.get(
											platforms[platform]);

									if (localeMap != null)
										for (int locale = 0;
											locale < locales.length
												&& locale < 0xFF;
											locale++) {
											Set commandIds =
												(Set) localeMap.get(
													locales[locale]);

											if (commandIds != null) {
												String commandId =
													commandIds.size() == 1
														? (String) commandIds
															.iterator()
															.next()
														: null;

												if (assignment == null)
													assignment =
														new Assignment();

												switch (rank) {
													case 0 :
														if (keyConfiguration
															== 0
															&& !assignment
																.hasPreferenceCommandIdInFirstKeyConfiguration) {
															assignment
																.hasPreferenceCommandIdInFirstKeyConfiguration =
																true;
															assignment
																.preferenceCommandIdInFirstKeyConfiguration =
																commandId;
														} else if (
															!assignment
																.hasPreferenceCommandIdInInheritedKeyConfiguration) {
															assignment
																.hasPreferenceCommandIdInInheritedKeyConfiguration =
																true;
															assignment
																.preferenceCommandIdInInheritedKeyConfiguration =
																commandId;
														}

														break;

													case 1 :
														if (keyConfiguration
															== 0
															&& !assignment
																.hasPluginCommandIdInFirstKeyConfiguration) {
															assignment
																.hasPluginCommandIdInFirstKeyConfiguration =
																true;
															assignment
																.pluginCommandIdInFirstKeyConfiguration =
																commandId;
														} else if (
															!assignment
																.hasPluginCommandIdInInheritedKeyConfiguration) {
															assignment
																.hasPluginCommandIdInInheritedKeyConfiguration =
																true;
															assignment
																.pluginCommandIdInInheritedKeyConfiguration =
																commandId;
														}

														break;
												}
											}
										}
								}

						}

				}

			if (assignment != null)
				assignmentsByContextId.put(contextId, assignment);
		}
	}

	/**
     * <p>
     * Finds a single match for this key binding node, given the state passed in
     * as parameters. The match will contain a single command identifier, and a
     * match value (a rough approximation of how accurate the match may be --
     * lower is better).
     * </p>
     * <p>
     * The matching algorithm checks for a match in each context given -- in the
     * order provided. Similar, it checks the key configuration, the rank (see
     * below), the platform, and the locale -- in the order provided. If a
     * single command identifier (note: this could be <code>null</code>--
     * indicating a removed key binding) matches the given characteristics, then
     * it is added to the list. For each context identifier, there can be at
     * most one match. If there is no match,
     * <em>or the match has a <code>null</code> command identifier</em>,
     * then we will move on to consider the next context.
     * </p>
     * <p>
     * The rank is a special value. It is either <code>0</code> or
     * <code>1</code>. It is used for marking removed key bindings. The
     * removed command identifier is moved to the first rank, and a
     * <code>null</code> command identifier is placed in the zeroth rank. The
     * ranking mechanism is controlled externally, but only the zeroth and first
     * ranks are considered internally.
     * </p>
     * <p>
     * When this method completes, <code>match</code> will contain the best
     * possible match, if any. If no match can be found, then <code>match</code>
     * will be <code>null</code>.
     * </p>
     * <p>
     * TODO Doug's Note (May 29, 2004): This mechanism is insanely complex for
     * what it is doing, and doesn't seem to cover all of the use cases. This
     * would be a good candidate for refactoring in the future. Most of this
     * code was written by Chris McLaren, but I've had to hack in some
     * behavioural changes to accomodate bugs for 3.0. If you're looking for
     * insight as to how it currently works, you might try talking to one of us.
     * Most notably, the fall through mechanism for <code>null</code> command
     * identifiers. My main concerns are: interactions between key
     * configurations, and unbinding a key in a child context so as to bind it
     * in a parent context.
     * </p>
     * 
     * @param contextTree
     *            The tree of contexts to consider. The tree is represented as a
     *            map of child context identifiers to parent context
     *            identifiers. This value must never be <code>null</code>,
     *            though it may contain <code>null</code> parents. It should
     *            never contain <code>null</code> children. It should only
     *            contain strings.
     * @param keyConfigurationIds
     *            The key configuration identifiers in the order they should be
     *            considered. This value must never be <code>null</code>,
     *            though it may contain <code>null</code> values.
     *            <code>null</code> values typically indicate "any".
     * @param platforms
     *            The platform identifiers in the order they should be
     *            considered. This value must never be <code>null</code>,
     *            though it may contain <code>null</code> values.
     *            <code>null</code> values typically indicate "any".
     * @param locales
     *            The locale identifiers in the order they should be considered.
     *            This value must never be <code>null</code>, though it may
     *            contain <code>null</code> values. <code>null</code> values
     *            typically indicate "any".
     */
    private void solveMatch(Map contextTree,
            String[] keyConfigurationIds, String[] platforms, String[] locales) {
        // Clear out the current match.
        match = null;
        
        // Get an array of context identifiers.
        final String[] contextIds = (String[]) contextTree.keySet().toArray(
                new String[contextTree.size()]); 

        // Get the maximum indices to consider.
        final int maxContext = (contextIds.length > 0xFF) ? 0xFF
                : contextIds.length;
        int maxKeyConfiguration = (keyConfigurationIds.length > 0xFF) ? 0xFF
                : keyConfigurationIds.length;
        final int maxPlatform = (platforms.length > 0xFF) ? 0xFF
                : platforms.length;
        final int maxLocale = (locales.length > 0xFF) ? 0xFF : locales.length;

        // Peel apart the nested map looking for matches.
        final Collection contextIdsNotToConsider = new HashSet();
        for (int context = 0; context < maxContext; context++) {
            boolean matchFoundForThisContext = false;

            /*
             * Check to see if the context identifier has been nixed by a child
             * context. That is, has a non-null match been found in a child
             * context?
             */
            final String contextId = contextIds[context];
            if (contextIdsNotToConsider.contains(contextId)) {
                continue;
            }

            final Map keyConfigurationMap = (Map) contextMap.get(contextId);
            if (keyConfigurationMap != null)
                    for (int keyConfiguration = 0; keyConfiguration < maxKeyConfiguration
                            && !matchFoundForThisContext; keyConfiguration++) {
                        final Map rankMap = (Map) keyConfigurationMap
                                .get(keyConfigurationIds[keyConfiguration]);

                        if (rankMap != null) {
                            for (int rank = 0; rank <= 1; rank++) {
                                final Map platformMap = (Map) rankMap
                                        .get(new Integer(rank));

                                if (platformMap != null)
                                        for (int platform = 0; platform < maxPlatform
                                                && !matchFoundForThisContext; platform++) {
                                            final Map localeMap = (Map) platformMap
                                                    .get(platforms[platform]);

                                            if (localeMap != null)
                                                    for (int locale = 0; locale < maxLocale
                                                            && !matchFoundForThisContext; locale++) {
                                                        final Set commandIds = (Set) localeMap
                                                                .get(locales[locale]);

                                                        if (commandIds != null) {
                                                            /*
                                                             * Jump to the next
                                                             * context.
                                                             */
                                                            matchFoundForThisContext = true;

                                                            /*
                                                             * Get the command
                                                             * identifier.
                                                             */
                                                            final String commandId = commandIds
                                                                    .size() == 1 ? (String) commandIds
                                                                    .iterator()
                                                                    .next()
                                                                    : null;

                                                            /*
                                                             * Make sure we
                                                             * don't consider
                                                             * any higher key
                                                             * configurations
                                                             * again.
                                                             */
                                                            maxKeyConfiguration = keyConfiguration + 1;

                                                            /*
                                                             * Make sure we
                                                             * don't consider
                                                             * any parents of
                                                             * this context, if
                                                             * the command
                                                             * identifier is not
                                                             * null.
                                                             */
                                                            if (commandId != null) {
                                                                String parentContext = (String) contextTree
                                                                        .get(contextId);
                                                                while (parentContext != null) {
                                                                    contextIdsNotToConsider
                                                                            .add(parentContext);
                                                                    parentContext = (String) contextTree
                                                                            .get(parentContext);
                                                                }

                                                                /*
                                                                 * Check for a
                                                                 * conflict.
                                                                 */
                                                                if ((match != null)
                                                                        && (!contextIdsNotToConsider
                                                                                .contains(contextIds[match
                                                                                        .getValue() >> 24]))) {
                                                                    WorkbenchPlugin
                                                                            .log("Conflicting key binding for '" //$NON-NLS-1$
                                                                                    + commandId
                                                                                    + "' and '" //$NON-NLS-1$
                                                                                    + match
                                                                                            .getCommandId()
                                                                                    + "'"); //$NON-NLS-1$
                                                                    match = null;

                                                                } else {
                                                                    match = new Match(
                                                                            commandId,
                                                                            (context << 24)
                                                                                    + (keyConfiguration << 16)
                                                                                    + (platform << 8)
                                                                                    + locale);
                                                                }
                                                            }
                                                        }
                                                    }
                                        }
                            }
                        }
                    }
        }
    }
}
