/* ###
 * 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.app.plugin.core.datamgr.editor;

import java.util.*;

import docking.ComponentProvider;
import docking.actions.DockingToolActions;
import docking.actions.SharedDockingActionPlaceholder;
import ghidra.app.plugin.core.compositeeditor.*;
import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin;
import ghidra.app.plugin.core.datamgr.actions.AbstractFindReferencesToFieldAction;
import ghidra.app.plugin.core.function.AbstractEditFunctionSignatureDialog;
import ghidra.framework.model.DomainObject;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.data.*;
import ghidra.program.model.data.Enum;
import ghidra.program.model.listing.*;
import ghidra.util.*;
import ghidra.util.exception.*;

/**
 * Manages program and archive data type editors.
 */
public class DataTypeEditorManager implements EditorListener {

	private List<EditorProvider> editorList;
	private EditorOptionManager editorOptionMgr; // manages editor tool options
	private DataTypeManagerPlugin plugin;

	/**
	 * Constructs a manager for data type editors.
	 * @param plugin the plugin that owns this editor manager
	 */
	public DataTypeEditorManager(DataTypeManagerPlugin plugin) {
		this.plugin = plugin;
		initialize();
	}

	/**
	 * Dismisses all open editors without prompting to save any changes.
	 * Performs any other cleanup necessary for this manager.
	 */
	public void dispose() {
		// Close all editors without checking for changes.
		dismissEditors(null);
		editorOptionMgr.dispose();
	}

	/**
	 * Gets the location of the help for editing the specified data type.
	 * @param dataType the data type to be edited.
	 * @return the help location for editing the data type.
	 */
	public HelpLocation getEditorHelpLocation(DataType dataType) {
		if (dataType instanceof Enum) {
			return new HelpLocation("DataTypeEditors", "EnumEditor");
		}
		if ((dataType instanceof Union) || (dataType instanceof Structure)) {
			return new HelpLocation("DataTypeEditors", "Structure_Editor");
		}
		// default
		return new HelpLocation("DataTypeEditors", "Structure_Editor");
	}

	/**
	 * Determine if the indicated data type can be edited
	 * (i.e. it has an editor that this service knows how to invoke).
	 * @param dt data type to be edited
	 * @return true if this service can invoke an editor for changing the data type.
	 */
	public boolean isEditable(DataType dt) {
		if ((dt instanceof Enum) || (dt instanceof Union) || (dt instanceof Structure)) {
			return true;
		}
		return false;
	}

	/**
	 * Displays a data type editor for editing the indicated data type. If the data type is
	 * is already being edited then it is brought to the front. Otherwise, a new editor is created
	 * and displayed.
	 * @param dataType the data type to edit.
	 */
	public void edit(DataType dataType) {

		EditorProvider editor = reuseExistingEditor(dataType);
		if (editor != null) {
			return;
		}

		if (dataType instanceof Enum) {
			editor = new EnumEditorProvider(plugin, (Enum) dataType);
		}
		else if (dataType instanceof Union) {
			editor = new UnionEditorProvider(plugin, (Union) dataType, showUnionNumbersInHex());
		}
		else if (dataType instanceof Structure) {
			editor = new StructureEditorProvider(plugin, (Structure) dataType,
				showStructureNumbersInHex());
		}
		else if (dataType instanceof FunctionDefinition) {
			editFunctionSignature((FunctionDefinition) dataType);
		}

		if (editor != null) {
			editor.addEditorListener(this);
			editorList.add(editor);
		}
	}

	/**
	 * Displays a data type editor for editing the given Structure. If the structure is already 
	 * being edited then it is brought to the front. Otherwise, a new editor is created and 
	 * displayed.
	 * @param composite the structure.
	 * @param fieldName the optional name of the field to select in the editor.
	 */
	@SuppressWarnings("rawtypes") // ignore on CompositeEditorProvider
	public void edit(Composite composite, String fieldName) {

		CompositeEditorProvider editor = (CompositeEditorProvider) getEditor(composite);
		if (editor != null) {
			reuseExistingEditor(composite);
			editor.selectField(fieldName);
			return;
		}
		if (composite instanceof Union) {
			editor = new UnionEditorProvider(plugin, (Union) composite, showUnionNumbersInHex());
		}
		else if (composite instanceof Structure) {
			editor = new StructureEditorProvider(plugin, (Structure) composite,
				showStructureNumbersInHex());
		}

		editor.addEditorListener(this);
		editorList.add(editor);

		editor.selectField(fieldName);
	}

	private EditorProvider reuseExistingEditor(DataType dataType) {
		DataTypeManager dataTypeManager = dataType.getDataTypeManager();
		if (dataTypeManager == null) {
			throw new IllegalArgumentException(
				"Datatype " + dataType.getName() + " doesn't have a data type manager specified.");
		}

		CategoryPath categoryPath = dataType.getCategoryPath();
		if (categoryPath == null) {
			throw new IllegalArgumentException(
				"DataType " + dataType.getName() + " has no category path!");
		}

		EditorProvider editor = getEditor(dataType);
		if (editor != null) {
			ComponentProvider componentProvider = editor.getComponentProvider();
			plugin.getTool().showComponentProvider(componentProvider, true);
			componentProvider.toFront();
		}
		return editor;
	}

	private void installEditorActions() {

		// composite editor actions
		registerAction(ApplyAction.ACTION_NAME);
		registerAction(InsertUndefinedAction.ACTION_NAME);
		registerAction(MoveUpAction.ACTION_NAME);
		registerAction(MoveDownAction.ACTION_NAME);
		registerAction(ClearAction.ACTION_NAME);
		registerAction(DuplicateAction.ACTION_NAME);
		registerAction(DuplicateMultipleAction.ACTION_NAME);
		registerAction(DeleteAction.ACTION_NAME);
		registerAction(PointerAction.ACTION_NAME);
		registerAction(ArrayAction.ACTION_NAME);
		registerAction(AbstractFindReferencesToFieldAction.BASE_ACTION_NAME);
		registerAction(UnpackageAction.ACTION_NAME);
		registerAction(EditComponentAction.ACTION_NAME);
		registerAction(EditFieldAction.ACTION_NAME);
		registerAction(HexNumbersAction.ACTION_NAME);
		registerAction(CreateInternalStructureAction.ACTION_NAME);
		registerAction(ShowComponentPathAction.ACTION_NAME);
		registerAction(AddBitFieldAction.ACTION_NAME);
		registerAction(EditBitFieldAction.ACTION_NAME);
		registerAction(ShowDataTypeInTreeAction.ACTION_NAME);

		// enum editor actions
		registerAction(EnumEditorProvider.ACTION_NAME_ADD);
		registerAction(EnumEditorProvider.ACTION_NAME_APPLY);
		registerAction(EnumEditorProvider.ACTION_NAME_DELETE);
	}

	private void registerAction(String name) {
		DockingToolActions toolActions = plugin.getTool().getToolActions();
		toolActions.registerSharedActionPlaceholder(new DtSharedActionPlaceholder(name));
	}

	/**
	 * Checks for editor changes that have not been saved to the data type and prompts the user to save
	 * them if necessary. It then closes the editor.
	 * @param editor the editor we want to close.
	 * @param allowCancel true indicates that the user can cancel the editor close when prompted
	 * for whether to save changes or not.
	 * @return true if the editor is closed.
	 */
	boolean closeEditor(EditorProvider editor, boolean allowCancel) {
		if (checkEditor(editor, allowCancel)) {
			dismissEditor(editor);
			return true;
		}
		return false;
	}

	/**
	 * Get a list of data type path names for data types that are currently being edited
	 * @return a list of data type path names for data types that are currently being edited.
	 */
	public List<DataTypePath> getEditsInProgress() {
		List<DataTypePath> paths = new ArrayList<>();
		for (EditorProvider editor : editorList) {
			paths.add(editor.getDtPath());
		}
		return paths;
	}

	/**
	 * Get the category for the data type being edited; the data type
	 * may be new and not yet added to the category
	 * @param dataTypePath the full path name of the data type that is being
	 * edited if it were written to the category for this editor.
	 * @return category associated with the data type or null.
	 */
	public Category getEditedDataTypeCategory(DataTypePath dataTypePath) {
		CategoryPath categoryPath = dataTypePath.getCategoryPath();
		for (EditorProvider editor : editorList) {
			if (dataTypePath.equals(editor.getDtPath())) {
				DataTypeManager dtMgr = editor.getDataTypeManager();
				if (dtMgr.containsCategory(categoryPath)) {
					return dtMgr.getCategory(categoryPath);
				}
			}
		}
		return null;
	}

	/**
	 * Determines whether this manager has any data type editor sessions in progress.
	 * @return true if there are any data type editors.
	 */
	public boolean isEditInProgress() {
		return editorList.size() > 0;
	}

	/**
	 * Check for any data types being edited for the given data
	 * type manager and closes those editors. An editor is associated with a data type
	 * manager based on the data type manager for the category where the edits will be saved.
	 * If dtMgr is null then all editors will be dismissed.
	 * @param dtMgr the data type manager whose editors are to be dismissed.
	 * If null, then dismiss all editors.
	 */
	public void dismissEditors(DataTypeManager dtMgr) {
		ArrayList<EditorProvider> list = new ArrayList<>();
		for (EditorProvider editor : editorList) {
			DataTypeManager editorDtm = editor.getDataTypeManager();
			if ((dtMgr == null) || (dtMgr == editorDtm)) {
				list.add(editor);
			}
		}
		for (EditorProvider element : list) {
			dismissEditor(element);
		}
	}

	/**
	 * Closes the data type editor for the indicated data type.
	 * @param editor the editor to close.
	 */
	void dismissEditor(EditorProvider editor) {
		if (editor != null) {
			editor.dispose();
		}
		editorList.remove(editor); // Should the remove happen here or via the EditorListener?
	}

	/**
	 * Check for data types being edited for the given data type manager and prompt the user to
	 * save any unsaved changes.
	 * If dtMgr is null then all editors will be checked.
	 * @param dtMgr the data type manager whose editors are to be checked for changes.
	 * If null, then check all editors for save.
	 * @param allowCancel true indicates that the user can cancel the editor close when prompted
	 * for whether to save changes or not.
	 * @return true if all editors were resolved and can close now; return
	 * false if the user canceled when prompted to save changes.
	 */
	public boolean checkEditors(DataTypeManager dtMgr, boolean allowCancel) {
		for (EditorProvider editor : editorList) {
			DataTypeManager editorDtm = editor.getDataTypeManager();
			if (dtMgr == null || dtMgr == editorDtm) {
				if (!checkEditor(editor, allowCancel)) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Determines if the indicated editor has unsaved changes.
	 * Prompts the user to save the changes.
	 * @param editor the editor to check.
	 * @param allowCancel true indicates that the user can cancel the editor close when prompted
	 * for whether to save changes or not.
	 * @return true when done checking or false if the user cancels.
	 */
	boolean checkEditor(EditorProvider editor, boolean allowCancel) {
		if (editor != null) {
			if (editor.needsSave()) {
				editor.show();
				if (!editor.checkForSave(allowCancel)) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Gets the manager for the data type editor options.
	 * @return the option manager
	 */
	EditorOptionManager getEditorOptionManager() {
		return editorOptionMgr;
	}

	/**
	 * Generate a unique data type name; Checks the data type manager for the indicated category
	 * to determine a unique name based on the specified base name. Also, checks the current
	 * edit sessions so we don't use a name already being edited.
	 * @param category the category where the data type will be saved after editing.
	 * @param baseName the base name of the data type
	 * @return the unique data type name.
	 */
	String getUniqueName(Category category, String baseName) {
		DataTypeManager dtm = category.getDataTypeManager();
		String uniqueName = dtm.getUniqueName(category.getCategoryPath(), baseName);
		int oneUpNumber = 0;
		int pos = uniqueName.lastIndexOf('_');
		if (pos > 0) {
			try {
				String numStr = uniqueName.substring(pos + 1);
				oneUpNumber = Integer.parseInt(numStr);
				baseName = uniqueName.substring(0, pos);
			}
			catch (NumberFormatException e) {
				// leave at 0
			}
		}

		while (nameExists(dtm, uniqueName)) {
			++oneUpNumber;
			uniqueName = baseName + "_" + oneUpNumber;
		}
		return uniqueName;
	}

	/**
	 * Determines if a data type, indicated by the path name, already exists in a data type manager.
	 * @param dtm the data type manager
	 * @param dtName data type path name
	 * @return true if the named data type exists.
	 */
	public boolean nameExists(DataTypeManager dtm, String dtName) {
		for (EditorProvider editor : editorList) {
			if (editor.getDataTypeManager() == dtm) {
				DataTypePath dtPath = editor.getDtPath();
				if (dtPath.getDataTypeName().equals(dtName)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * If the specified data type is being edited for the indicated category, this gets that editor.
	 * @param dataType the data type
	 * @return the editor or null.
	 */
	public EditorProvider getEditor(DataType dataType) {
		DataTypeManager catDtm = dataType.getDataTypeManager();
		DataTypePath dataTypePath = dataType.getDataTypePath();
		for (EditorProvider editor : editorList) {
			if (editor.getDataTypeManager() == catDtm && editor.isEditing(dataTypePath)) {
				return editor;
			}
		}
		return null;
	}

	/**
	 * Create the objects we need for this plugin; register the service
	 * provided, create the actions, etc.
	 */
	private void initialize() {
		editorList = new ArrayList<>();
		editorOptionMgr = new EditorOptionManager(plugin);

		installEditorActions();
	}

	/**
	 * EditorListener method that gets called whenever an editor is closed.
	 * @param editor the data type editor that closed
	 */
	@Override
	public void closed(EditorProvider editor) {
		editorList.remove(editor);
	}

	/**
	 * Determines whether there are any editors for data types that will be written to the
	 * indicated data type manager and that have unsaved changes.
	 * @param dtMgr the data type manager
	 * @return true if there are unsaved changes.
	 */
	boolean hasEditorChanges(DataTypeManager dtMgr) {
		for (EditorProvider editor : editorList) {
			DataTypeManager editorDtm = editor.getDataTypeManager();
			if (dtMgr == null || dtMgr == editorDtm) {
				if (editor.needsSave()) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Closes all open editors regardless of whether there are unsaved changes.
	 */
	void close() {
		dismissEditors(null);
	}

	/**
	 * Determines whether the indicated domain object can be closed. The user will be prompted
	 * to save any editor changes. If the user cancels when prompted to save then the domain
	 * object should not close.
	 * @param dObj the domain object
	 * @return true if it can close.
	 */
	protected boolean canCloseDomainObject(DomainObject dObj) {
		if (dObj instanceof Program) {
			Program p = (Program) dObj;
			return checkEditors(p.getListing().getDataTypeManager(), true);
		}
		return true;
	}

	public boolean showStructureNumbersInHex() {
		return editorOptionMgr.showStructureNumbersInHex();
	}

	public boolean showUnionNumbersInHex() {
		return editorOptionMgr.showUnionNumbersInHex();
	}

	public void createNewStructure(Category category, boolean isPacked) {
		String newName = getUniqueName(category, "struct");
		DataTypeManager dataTypeManager = category.getDataTypeManager();
		SourceArchive sourceArchive = dataTypeManager.getLocalSourceArchive();
		StructureDataType structureDataType =
			new StructureDataType(category.getCategoryPath(), newName, 0, dataTypeManager);
		structureDataType.setSourceArchive(sourceArchive);
		structureDataType.setPackingEnabled(isPacked);
		edit(structureDataType);
	}

	public void createNewUnion(Category category, boolean isPacked) {
		String newName = getUniqueName(category, "union");
		DataTypeManager dataTypeManager = category.getDataTypeManager();
		SourceArchive sourceArchive = dataTypeManager.getLocalSourceArchive();
		UnionDataType unionDataType =
			new UnionDataType(category.getCategoryPath(), newName, dataTypeManager);
		unionDataType.setSourceArchive(sourceArchive);
		unionDataType.setPackingEnabled(isPacked);
		edit(unionDataType);
	}

	public void createNewEnum(Category category) {
		String newName = getUniqueName(category, "enum");
		DataTypeManager dataTypeManager = category.getDataTypeManager();
		SourceArchive sourceArchive = dataTypeManager.getLocalSourceArchive();
		DataType dataType =
			new EnumDataType(category.getCategoryPath(), newName, 1, dataTypeManager);
		dataType.setSourceArchive(sourceArchive);
		edit(dataType);
	}

	public void createNewFunctionDefinition(Category cat) {
		editFunctionSignature(cat, null);
	}

	public void editFunctionSignature(final FunctionDefinition functionDefinition) {
		DataTypeManager dataTypeManager = functionDefinition.getDataTypeManager();
		if (dataTypeManager == null) {
			throw new IllegalArgumentException("DataType " + functionDefinition.getPathName() +
				" has no DataTypeManager!  Make sure the " +
				"given DataType has been resolved by a DataTypeManager");
		}
		CategoryPath categoryPath = functionDefinition.getCategoryPath();
		Category category = dataTypeManager.getCategory(categoryPath);
		if (categoryPath == null) {
			throw new IllegalArgumentException(
				"DataType " + functionDefinition.getName() + " has no category path!");
		}
		editFunctionSignature(category, functionDefinition);
	}

	private void editFunctionSignature(Category category, FunctionDefinition functionDefinition) {
		PluginTool tool = plugin.getTool();
		DTMEditFunctionSignatureDialog editSigDialog = new DTMEditFunctionSignatureDialog(
			plugin.getTool(), "Edit Function Signature", category, functionDefinition);
		editSigDialog
				.setHelpLocation(new HelpLocation("DataTypeManagerPlugin", "Function_Definition"));
		tool.showDialog(editSigDialog);
	}

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

	/**
	 * <code>DTMEditFunctionSignatureDialog</code> provides the ability to edit the
	 * function signature associated with a specific {@link FunctionDefinition}.
	 * Use of this editor requires the presence of the tool-based datatype manager service.
	 */
	private class DTMEditFunctionSignatureDialog extends AbstractEditFunctionSignatureDialog {
		private final FunctionDefinition functionDefinition; // may be null
		private final FunctionSignature oldSignature;
		private final Category category;

		/**
		 * Construct function signature editor model
		 * @param pluginTool plugin tool
		 * @param title Dialog title
		 * @param category datatype category
		 * @param functionDefinition function definition to be modified (null for new definition)
		 */
		DTMEditFunctionSignatureDialog(PluginTool pluginTool, String title, Category category,
				FunctionDefinition functionDefinition) {
			super(pluginTool, title, false, true, false);
			this.functionDefinition = functionDefinition;
			this.category = category;
			this.oldSignature = buildSignature();
			if (isAdhocCallingConventionPermitted()) {
				callingConventionComboBox.setEditable(true);
			}
		}

		/**
		 * Determine if an adhoc calling convention entry is permitted (i.e., text entry)
		 * @return true if calling convention name may be edited with text entry, else false
		 */
		private boolean isAdhocCallingConventionPermitted() {
			// DataTypeManager dtm = functionDefinition.getDataTypeManager();
			// return dtm == null || dtm.getProgramArchitecture() == null;
			// TODO: not sure we should allow unrestricted entries which could lead to using misspelled names
			return false;
		}

		private FunctionSignature buildSignature() {
			if (functionDefinition != null) {
				if (category.getDataTypeManager() != functionDefinition.getDataTypeManager()) {
					throw new IllegalArgumentException(
						"FunctionDefinition and Category must have same DataTypeManager");
				}
				return functionDefinition;
			}
			return new FunctionDefinitionDataType("newFunction", category.getDataTypeManager());
		}

		@Override
		protected boolean hasNoReturn() {
			return functionDefinition != null ? functionDefinition.hasNoReturn() : false;
		}

		@Override
		protected String[] getSupportedCallFixupNames() {
			return null; // Call fixup not supported on FunctionDefinition
		}

		@Override
		protected String getCallFixupName() {
			return null; // Call fixup not supported on FunctionDefinition
		}

		@Override
		protected FunctionSignature getFunctionSignature() {
			return oldSignature;
		}

		@Override
		protected String getPrototypeString() {
			return getFunctionSignature().getPrototypeString();
		}

		@Override
		protected String getCallingConventionName() {
			return getFunctionSignature().getCallingConventionName();
		}

		@Override
		protected List<String> getCallingConventionNames() {
			// can't rely on functionDefinition which may be null for new definition
			DataTypeManager dtm = getDataTypeManager();
			if (dtm instanceof CompositeViewerDataTypeManager cvdtm) {
				dtm = cvdtm.getOriginalDataTypeManager();
			}
			ArrayList<String> list = new ArrayList<>();
			list.add(Function.UNKNOWN_CALLING_CONVENTION_STRING);
			list.add(Function.DEFAULT_CALLING_CONVENTION_STRING);
			list.addAll(dtm.getDefinedCallingConventionNames());
			return list;
		}

		@Override
		protected DataTypeManager getDataTypeManager() {
			return category.getDataTypeManager();
		}

		@Override
		protected boolean applyChanges() {
			// can't use a command here as we have to create a transaction on the datatypeManager
			// (it might be an archive and the transaction on the program wouldn't work)
			FunctionDefinitionDataType newDefinition = null;
			try {
				newDefinition = parseSignature();
			}
			catch (CancelledException e1) {
				// ignore
			}

			if (newDefinition == null) {
				return false;
			}

			String callingConvention = getCallingConvention();
			boolean hasNoReturn = hasNoReturnSelected();
			try {
				newDefinition.setCallingConvention(callingConvention);
				newDefinition.setNoReturn(hasNoReturn);

				DataTypeManager manager = getDataTypeManager();
				SourceArchive sourceArchive = manager.getLocalSourceArchive();
				if (functionDefinition == null) {
					newDefinition.setSourceArchive(sourceArchive);
					newDefinition.setCategoryPath(category.getCategoryPath());
					int id = manager.startTransaction("Create Function Definition");
					try {
						manager.addDataType(newDefinition, DataTypeConflictHandler.REPLACE_HANDLER);
					}
					finally {
						manager.endTransaction(id, true);
					}
				}
				else {
					int id = manager.startTransaction("Edit Function Definition");
					try {
						if (!functionDefinition.getName().equals(newDefinition.getName())) {
							functionDefinition.setName(newDefinition.getName());
						}
						functionDefinition.setArguments(newDefinition.getArguments());
						if (!Objects.equals(callingConvention,
							functionDefinition.getCallingConventionName())) {
							functionDefinition.setCallingConvention(callingConvention);
						}
						functionDefinition.setReturnType(newDefinition.getReturnType());
						functionDefinition.setVarArgs(newDefinition.hasVarArgs());
						functionDefinition.setNoReturn(hasNoReturn);
					}
					catch (InvalidNameException | DuplicateNameException e) {
						// not sure why we are squashing this? ...assuming this can't happen
						Msg.error(this, "Unexpected Exception", e);
					}
					finally {
						manager.endTransaction(id, true);
					}
				}
			}
			catch (InvalidInputException e) {
				setStatusText("Unknown calling convention specified: " + callingConvention,
					MessageType.ERROR);
				return false;
			}

			return true;
		}
	}

	// small class to register actions by name before the various editors have been shown
	private class DtSharedActionPlaceholder implements SharedDockingActionPlaceholder {

		private String name;

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

		@Override
		public String getOwner() {
			// all of our shared actions belong to the plugin
			return plugin.getName();
		}

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