/* ###
 * IP: GHIDRA
 *
 * 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 ghidra.framework.plugintool.mgr;

import java.beans.PropertyChangeListener;
import java.util.*;

import javax.swing.JComponent;
import javax.swing.tree.TreePath;

import org.jdom2.Element;

import docking.actions.DockingToolActions;
import docking.actions.SharedDockingActionPlaceholder;
import docking.options.OptionsService;
import docking.options.editor.OptionsDialog;
import docking.tool.ToolConstants;
import docking.tool.util.DockingToolConstants;
import ghidra.framework.options.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.dialog.KeyBindingsPanel;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.exception.AssertException;

/**
 * Created by PluginTool to manage the set of Options for each category.
 */
public class OptionsManager implements OptionsService, OptionsChangeListener {

	private OptionsDialog optionsDialog;
	private PluginTool tool;
	private Map<String, ToolOptions> optionsMap;

	private OptionsSharedActionPlaceholder toggleActionPlaceholder;

	/**
	 * Constructor
	 * @param tool associated with this OptionsManager
	 */
	public OptionsManager(PluginTool tool) {
		this.tool = tool;
		optionsMap = new HashMap<>();
	}

	private void createActions() {

		if (toggleActionPlaceholder != null) {
			return;
		}

		// This action is created later in the OptionsPanel
		toggleActionPlaceholder =
			new OptionsSharedActionPlaceholder(OptionsDialog.TOGGLE_EDITOR_ACTION_NAME);
		DockingToolActions toolActions = tool.getToolActions();
		toolActions.registerSharedActionPlaceholder(toggleActionPlaceholder);
	}

	public void dispose() {
		if (optionsDialog != null) {
			optionsDialog.dispose();
		}
		optionsMap.values().forEach(options -> options.dispose());
	}

	@Override
	public ToolOptions getOptions(String category) {

		if (category.contains(Options.DELIMITER_STRING)) {
			throw new AssertException(
				"Options category cannot contain the options path delimiter '" + Options.DELIMITER +
					"'");
		}

		ToolOptions opt = optionsMap.get(category);
		if (opt == null) {
			opt = new ToolOptions(category);
			opt.addOptionsChangeListener(this);
			optionsMap.put(category, opt);
		}
		return opt;
	}

	/**
	 * Updates saved options from an old name to a new name.  NOTE: this must be called before
	 * any calls to register or get options.
	 * @param oldName the old name of the options.
	 * @param newName the new name of the options.
	 */
	public void registerOptionNameChanged(String oldName, String newName) {
		if (optionsMap.containsKey(oldName)) {
			ToolOptions toolOptions = optionsMap.remove(oldName);
			toolOptions.setName(newName);
			optionsMap.put(newName, toolOptions);
		}
	}

	@Override
	public boolean hasOptions(String category) {
		return optionsMap.containsKey(category);
	}

	@Override
	public void showOptionsDialog(String category, String filterText) {
		if (optionsDialog != null && optionsDialog.isVisible()) {
			optionsDialog.toFront();
			return;
		}
		optionsDialog = createOptionsDialog();
		optionsDialog.displayCategory(category, filterText);
		tool.showDialog(optionsDialog);
	}

	@Override
	public ToolOptions[] getOptions() {
		ToolOptions[] opt = new ToolOptions[optionsMap.size()];
		int idx = 0;
		for (String key : optionsMap.keySet()) {
			opt[idx] = optionsMap.get(key);
			++idx;
		}
		Arrays.sort(opt, new OptionsComparator());
		return opt;
	}

	/**
	 * Write this object out; first remove any unused options so they
	 * do not hang around.
	 * @return XML element containing the state of all the options
	 */
	public Element getConfigState() {
		Element root = new Element("OPTIONS");
		for (String key : optionsMap.keySet()) {
			ToolOptions opt = optionsMap.get(key);
			if (hasNonDefaultValues(opt)) {
				root.addContent(opt.getXmlRoot(false));
			}
		}
		return root;
	}

	private boolean hasNonDefaultValues(Options options) {
		List<String> optionNames = options.getOptionNames();
		for (String string : optionNames) {
			if (!options.isDefaultValue(string)) {
				return true;
			}
		}
		return false;
	}

	public void removeUnusedOptions() {
		// 1st clean up any unused options before saving...
		List<String> deleteList = new ArrayList<>();
		for (String key : optionsMap.keySet()) {
			ToolOptions opt = optionsMap.get(key);
			opt.removeUnusedOptions();
			if (opt.getOptionNames().isEmpty()) {
				deleteList.add(opt.getName());
			}
		}
		removeUnusedOptions(deleteList);
	}

	/**
	 * Restore Options objects using the given XML Element.
	 * @param root element to use to restore the Options objects
	 */
	public void setConfigState(Element root) {
		Iterator<?> iter = root.getChildren().iterator();
		while (iter.hasNext()) {
			ToolOptions opt = new ToolOptions((Element) iter.next());
			ToolOptions oldOptions = optionsMap.get(opt.getName());
			if (oldOptions == null) {
				opt.addOptionsChangeListener(this);
			}
			else {
				opt.takeListeners(oldOptions);
				opt.registerOptions(oldOptions);
			}
			optionsMap.put(opt.getName(), opt);
		}
	}

	public void editOptions() {
		if (optionsMap.isEmpty()) {
			Msg.showInfo(getClass(), tool.getToolFrame(), "No Options",
				"No Options set in this tool");
			return;
		}
		if (optionsDialog != null && optionsDialog.isVisible()) {
			optionsDialog.toFront();
			return;
		}
		optionsDialog = createOptionsDialog();
		tool.showDialog(optionsDialog);
	}

	public void validateOptions() {
		for (ToolOptions options : optionsMap.values()) {
			options.validateOptions();
		}
	}

	private OptionsDialog createOptionsDialog() {

		if (optionsMap.size() == 0) {
			return null;
		}

		createActions();

		Options keyBindingOptions = getOptions(DockingToolConstants.KEY_BINDINGS);
		TreePath path = null;
		if (optionsDialog != null) {
			path = optionsDialog.getSelectedPath();
			optionsDialog.dispose();

			OptionsEditor oldEditor = keyBindingOptions.getOptionsEditor();
			oldEditor.dispose();
		}

		keyBindingOptions.registerOptionsEditor(() -> new KeyBindingOptionsEditor());
		String optionsName = "Options for " + tool.getName();
		Options[] options = getEditableOptions();
		OptionsDialog dialog = new OptionsDialog(optionsName, "Options", options, null, true);
		dialog.setSelectedPath(path);
		dialog.setHelpLocation(
			new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "ToolOptions_Dialog"));
		return dialog;
	}

	private Options[] getEditableOptions() {
		return tool.getOptions();
	}

	private void removeUnusedOptions(List<String> deleteList) {
		for (String name : deleteList) {
			ToolOptions options = optionsMap.remove(name);
			options.removeOptionsChangeListener(this);
		}
	}

	@Override
	public void optionsChanged(ToolOptions options, String name, Object oldValue, Object newValue) {
		tool.setConfigChanged(true);
	}

//=================================================================================================
// Inner Classes
//=================================================================================================	

	private class OptionsComparator implements Comparator<ToolOptions> {
		@Override
		public int compare(ToolOptions o1, ToolOptions o2) {
			return o1.getName().compareTo(o2.getName());
		}
	}

	private class KeyBindingOptionsEditor implements OptionsEditor {

		private KeyBindingsPanel panel;

		@Override
		public void apply() {
			panel.apply();
		}

		@Override
		public void cancel() {
			panel.cancel();
		}

		@Override
		public void reload() {
			panel.reload();
		}

		@Override
		public void dispose() {
			if (panel != null) {
				panel.dispose();
			}
		}

		@Override
		public JComponent getEditorComponent(Options options,
				EditorStateFactory editorStateFactory) {
			return panel;
		}

		@Override
		public void setOptionsPropertyChangeListener(PropertyChangeListener listener) {
			if (panel == null) {
				panel = new KeyBindingsPanel(tool);
			}
			panel.setOptionsPropertyChangeListener(listener);
		}
	}

	// Small class to register actions by name before the various editors have been shown. We need
	// this for the OptionsDialog actions to appear in the key binding options.
	private class OptionsSharedActionPlaceholder implements SharedDockingActionPlaceholder {

		private String name;

		OptionsSharedActionPlaceholder(String name) {
			this.name = name;
		}

		@Override
		public String getOwner() {
			return OptionsDialog.OPTIONS_OWNER;
		}

		@Override
		public String getName() {
			return name;
		}
	}
}
