/*
 * Copyright 2015 Patrick Ahlbrecht
 *
 * Licensed 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 de.onyxbits.weave.swing;

import java.util.HashMap;
import java.util.ResourceBundle;

import javax.swing.Action;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JSeparator;

/**
 * A builder that models the menu bar as a treelike data structure. This way,
 * the location of every menu component can be given by a path. For example, the
 * path "file/new" would identify the "New" operation in the "File" menu. The
 * following rules apply when specifying a path:
 * <ul>
 * <li>As path components may be used for localization, they may only consist of
 * characters that are legal as keys in {@link ResourceBundle}S (minus the '.'
 * character).
 * <li>A path component must consist of at least one character (e.g. "file//new"
 * is illegal).
 * <li>All paths are relative to the menubar (root component) and menubars don't
 * have a concept of "empty directories". Therefore there may not be leading or
 * trailing slashes.
 * <li>Path component names may be chosen arbitrarily (e.g. "datei/neu" instead
 * of "file/new"). They have no meaning to the builder. It is the hierarchy that
 * matters.
 * <li>Every menu component must have a unique path string (this goes for
 * separators as well)!
 * </ul>
 * 
 * @author patrick
 * 
 */
public class MenuBarBuilder {

	/**
	 * Separator to use for path components.
	 */
	public static final char PATHSEPARATOR = '/';

	private HashMap<String, JComponent> maps;
	private JMenuBar mbar;
	private ActionLocalizer localizer;
	private boolean fullPath;
	private String prefix;

	public MenuBarBuilder() {
		maps = new HashMap<String, JComponent>();
		mbar = new JMenuBar();
	}

	/**
	 * Use an {@link ActionLocalizer} to automatically localize items. Only items
	 * that are added after a call to this method will be localized. Likewise,
	 * setting the localizer to null again will stop localization for the
	 * following items.
	 * 
	 * @param localizer
	 *          the localizer to use. May be null.
	 * @return this reference for chaining
	 */
	public MenuBarBuilder withLocalizer(ActionLocalizer localizer) {
		this.localizer = localizer;
		return this;
	}

	/**
	 * Use the menu path (replacing '/' with '.') as the localization key? This
	 * only applies to items following this method call.
	 * 
	 * @param fp
	 *          true to use the full path, false to only use the last path
	 *          component as key.
	 * @return
	 */
	public MenuBarBuilder withFullPath(boolean fp) {
		fullPath = fp;
		return this;
	}

	/**
	 * Use a prefix when constructing the localization key. This applies only to
	 * items added after this call.
	 * 
	 * @param prefix
	 *          prefix (no trailing '.') or null.
	 * @return this reference for chaining.
	 */
	public MenuBarBuilder withPrefix(String prefix) {
		this.prefix = prefix;
		return this;
	}

	/**
	 * Create multiple menus at once through their path specification. NOTE: this
	 * method requires a localizer. Otherwise all menus will be blank.
	 * 
	 * @param paths
	 *          where to add
	 * @return this reference for chaining.
	 */
	public MenuBarBuilder withMenus(String... paths) {
		for (String path : paths) {
			addMenu(path,
					localizer.localize(new NoAction(), getLocalizationKey(path)));
		}
		return this;
	}

	/**
	 * Add an item/menu to the menubar. Items are appended to their parent
	 * container in the same order in which they are added. This implies that
	 * parent items must be created before children can be added.
	 * 
	 * @param path
	 *          where to add.
	 * @param item
	 *          what to add. NOTE: this should be an instance of {@link JMenu},
	 *          {@link JMenuItem}, {@link JRadioButtonMenuItem},
	 *          {@link JCheckBoxMenuItem} or {@link JSeparator}. Nesting menus is
	 *          perfectly fine (though not from a usability point of view).
	 * @return this reference for chaining.
	 */
	public MenuBarBuilder add(String path, JComponent item) {
		maps.put(path, item);
		if (localizer != null && item instanceof JMenuItem) {
			Action a = ((JMenuItem) item).getAction();
			localizer.localize(a, getLocalizationKey(path));
		}
		String parentPath = path.substring(0,
				Math.max(path.lastIndexOf(PATHSEPARATOR), 0));
		JComponent parent = maps.get(parentPath);
		if (item.getName() == null) {
			item.setName(path); // for unit tests
		}
		if (parent == null) {
			mbar.add(item);
		}
		else {
			parent.add(item);
		}
		return this;
	}

	private String getLocalizationKey(String path) {
		StringBuilder ret = new StringBuilder();
		if (prefix != null) {
			ret.append(prefix);
			ret.append(".");
		}
		if (fullPath) {
			ret.append(path.replaceAll("/", "."));
		}
		else {
			ret.append(path.substring(path.lastIndexOf('/') + 1));
		}
		return ret.toString();
	}

	/**
	 * Construct a {@link JMenu} and forward it to add().
	 * 
	 * @param path
	 *          where to add
	 * @param a
	 *          configuration
	 * @return this reference for chaining.
	 */
	public MenuBarBuilder addMenu(String path, Action a) {
		return add(path, new JMenu(a));
	}

	/**
	 * Construct a {@link JMenuItem} and forward it to add()
	 * 
	 * @param path
	 *          where to add
	 * @param a
	 *          configuration
	 * @return this reference for chaining.
	 */
	public MenuBarBuilder addItem(String path, Action a) {
		return add(path, new JMenuItem(a));
	}

	/**
	 * Construct a {@link JCheckBoxMenuItem} and forward it to add()
	 * 
	 * @param path
	 *          where to add
	 * @param a
	 *          configuration
	 * @return this reference for chaining.
	 */
	public MenuBarBuilder addCheckbox(String path, Action a) {
		return add(path, new JCheckBoxMenuItem(a));
	}

	/**
	 * Construct a {@link JRadioButtonMenuItem} and forward it to add()
	 * 
	 * @param path
	 *          where to add
	 * @param a
	 *          configuration
	 * @return this reference for chaining.
	 */
	public MenuBarBuilder addRadio(String path, Action a) {
		return add(path, new JRadioButtonMenuItem(a));
	}

	/**
	 * Construct a {@link JSeparator} and forward it to add()
	 * 
	 * @param path
	 *          where to add
	 * @return this reference for chaining.
	 */
	public MenuBarBuilder addSeparator(String path) {
		return add(path, new JSeparator());
	}

	/**
	 * Construct the configured menubar
	 * 
	 * @return the menubar.
	 */
	public JMenuBar build() {
		return mbar;
	}

}