/* ###
 * 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.main;

import static ghidra.framework.main.DataTreeDialogType.*;

import java.awt.*;
import java.awt.event.*;
import java.util.HashSet;
import java.util.Set;

import javax.swing.*;
import javax.swing.border.TitledBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

import docking.*;
import docking.event.mouse.GMouseListenerAdapter;
import docking.widgets.combobox.GComboBox;
import docking.widgets.label.GDLabel;
import docking.widgets.label.GLabel;
import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeTask;
import docking.widgets.tree.support.GTreeSelectionEvent;
import docking.widgets.tree.support.GTreeSelectionListener;
import ghidra.framework.main.datatree.*;
import ghidra.framework.main.projectdata.actions.*;
import ghidra.framework.model.*;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException;
import ghidra.util.layout.PairLayout;
import ghidra.util.task.TaskMonitor;

/**
 * Base dialog for choosing DomainFiles. Provides and manages the base data tree panel. Subclasses
 * should call the buildDataTreePanel() when they are constructing their main panels. They should
 * also call the initializeFocusedComponent() method so that default focus for the dialog is in
 * the text field if it is enabled or otherwise the focus should be the tree.
 */
public abstract class AbstractDataTreeDialog extends DialogComponentProvider
		implements GTreeSelectionListener, ActionListener {

	protected final static int WIDTH = 350;
	protected final static int HEIGHT = 500;

	protected ProjectDataTreePanel treePanel;

	private JComboBox<String> projectComboBox; // used for open data
	private ProjectLocator[] projectLocators;
	private DomainFileFilter filter;
	private JTextField nameField;
	private JLabel folderNameLabel;
	private ActionListener okActionListener;
	private DomainFolder domainFolder;
	private DomainFile domainFile;
	private DataTreeDialogType type;
	private Component parent;

	private boolean cancelled = false;

	private ProjectDataExpandAction<DialogProjectTreeContext> expandAction;
	private ProjectDataCollapseAction<DialogProjectTreeContext> collapseAction;
	private ProjectDataNewFolderAction<DialogProjectTreeContext> newFolderAction;

	private Integer treeSelectionMode;
	private final Project project;

	/**
	 * Construct a new DataTreeDialog for the given project.
	 *
	 * @param parent dialog's parent
	 * @param title title to use
	 * @param type specify OPEN, SAVE, CHOOSE_FOLDER, or CHOOSE_USER_FOLDER
	 * @param filter filter used to control what is displayed in the data tree
	 * @param project the project to browse
	 * @throws IllegalArgumentException if invalid type is specified
	 */
	protected AbstractDataTreeDialog(Component parent, String title, DataTreeDialogType type,
			DomainFileFilter filter, Project project) {
		super(title, true, true, true, false);

		this.project = project;
		this.parent = parent;
		this.type = type;
		this.filter = filter;

		initializeButtons();
		rootPanel.setPreferredSize(new Dimension(WIDTH, HEIGHT));

		createActions();
	}

	protected void initializeFocusedComponent() {
		Component focusComponent = nameField;
		if (!nameField.isEditable()) {
			focusComponent = treePanel.getFilterField();
		}
		setFocusComponent(focusComponent);
	}

	public void setTreeSelectionMode(int mode) {
		if (treePanel != null) {
			treePanel.getTreeSelectionModel().setSelectionMode(mode);
		}
		treeSelectionMode = mode;
	}

	private void initializeButtons() {
		addOKButton();
		addCancelButton();

		if (type == SAVE) {
			okButton.setText("Save");
			okButton.setMnemonic('S');
		}
		else if (type == CREATE) {
			okButton.setText("Create");
			okButton.setMnemonic('C');
		}
		setOkEnabled(false);

	}

	private void createActions() {
		String owner = "DataTreeDialogActions";

		String groupName = "Cut/copy/paste/new";
		newFolderAction = new DialogProjectDataNewFolderAction(owner, groupName);

		groupName = "Expand/Collapse";
		expandAction = new DialogProjectDataExpandAction(owner, groupName);
		collapseAction = new DialogProjectDataCollapseAction(owner, groupName);

		addAction(newFolderAction);
		addAction(expandAction);
		addAction(collapseAction);
	}

	/**
	 * Add action listener that is called when the OK button is hit.
	 * @param l listener to add
	 */
	public void addOkActionListener(ActionListener l) {
		okActionListener = l;
	}

	@Override
	public ActionContext getActionContext(MouseEvent event) {
		if (treePanel == null) {
			// must have been closed; some kind of timing issue
			return super.getActionContext(event);
		}

		ActionContext actionContext = treePanel.getActionContext(null, event);
		if (actionContext instanceof DialogActionContext dac) {
			dac.setDialogComponentProvider(this);
		}
		return actionContext;
	}

	public void show() {
		DockingWindowManager.showDialog(parent, this);
	}

	/**
	 * Shows this dialog.  The preferred show method is {@link #show()}, as it is the preferred 
	 * nomenclature.
	 */
	public void showComponent() {
		show();
	}

	public GTree getTree() {
		return treePanel.getDataTree();
	}

	public String getNameText() {
		return nameField.getText();
	}

	public String getFolderText() {
		return folderNameLabel.getText();
	}

	public void setNameText(String name) {
		// We need to run this code in a task since the tree may already be processing other tasks
		// that would override this setting when they are run.  But putting this task in the queue, 
		// we get the correct UI update ordering.
		DataTree tree = treePanel.getDataTree();
		tree.runTask(new SetNameTextTask(tree, name));
	}

	/**
	 * Sets a domain folder as the initially selected folder when the dialog is first shown.
	 *  
	 * @param folder {@link DomainFolder} to select when showing the dialog
	 */
	public void setSelectedFolder(DomainFolder folder) {
		if (folder != null) {
			treePanel.selectDomainFolder(folder);
		}
	}

	/**
	 * Get the selected domain file.
	 * @return null if there was no domain file selected
	 */
	public DomainFile getDomainFile() {

		if (domainFile != null) {
			return domainFile;
		}

		if (cancelled) {
			return null;
		}

		if (treePanel != null) {
			domainFile = treePanel.getSelectedDomainFile();
		}
		return domainFile;
	}

	/**
	 * Get the selected folder.
	 * @return null if there was no domain folder selected
	 */
	public DomainFolder getDomainFolder() {
		if (cancelled) {
			return null;
		}
		if (domainFolder == null) {
			domainFolder = treePanel.getSelectedDomainFolder();
		}
		return domainFolder;
	}

	@Override
	public void valueChanged(GTreeSelectionEvent e) {
		clearStatusText();

		if (type == CHOOSE_FOLDER) {
			updateFromTreeSelectionInFolderMode();
		}
		else {
			updateFromTreeSelectionInFileMode();
		}

		String text = nameField.getText();
		setOkEnabled((text != null) && !text.isEmpty());
	}

	private void updateFromTreeSelectionInFileMode() {
		DomainFile newFile = treePanel.getSelectedDomainFile();
		if (isFolderLink(newFile)) {
			updateFromFolderLink(newFile);
			return;
		}

		// not a folder link; see if we have a new file selected
		if (newFile != null) {
			domainFile = newFile;
			domainFolder = domainFile.getParent();
			String pathname = domainFolder.getPathname();
			folderNameLabel.setText(pathname);
			String filename = domainFile.getName();
			nameField.setText(filename);
			return;
		}

		// No selected domain file
		domainFile = null;
		domainFolder = getSelectedFolder();
		String pathname = domainFolder.getPathname();
		folderNameLabel.setText(pathname);
		updateNameFieldTextForNoFileSelected();
	}

	private void updateNameFieldTextForNoFileSelected() {
		if (!nameField.isEditable()) {
			nameField.setText("");
			return;
		}

		if (nameField.getText().length() > 0) {
			nameField.selectAll();
		}
	}

	private void updateFromFolderLink(DomainFile newFile) {

		LinkFileInfo linkInfo = newFile.getLinkInfo();
		DomainFolder folder = newFile.getParent();
		String filename = newFile.getName();

		// Ensure we don't have a folder name conflict
		if (folder.getFolder(filename) == null) {
			domainFolder = linkInfo.getLinkedFolder();
			if (domainFolder == null) {
				domainFolder = getSelectedFolder();
			}

			domainFile = null;
			folderNameLabel.setText(domainFolder.getPathname());
			updateNameFieldTextForNoFileSelected();
			return;
		}

		domainFile = newFile;
		folderNameLabel.setText(folder.getPathname());
		nameField.setText(filename);
		domainFolder = folder;
	}

	private boolean isFolderLink(DomainFile file) {
		if (file == null) {
			return false;
		}
		LinkFileInfo linkInfo = file.getLinkInfo();
		return linkInfo != null && linkInfo.isFolderLink();
	}

	private void updateFromTreeSelectionInFolderMode() {

		// The tree selection has changed and we are in FOLDER mode.  Update the folder selection
		// based on type of node selected.
		domainFolder = getSelectedFolder();
		DomainFolder folderParent = domainFolder.getParent();
		if (folderParent == null) {
			folderParent = domainFolder; // root folder; no parent
		}

		folderNameLabel.setText(folderParent.getPathname());
		nameField.setText(domainFolder.getName());

	}

	/**
	 * Returns the selected folder, or the selected file's parent or the root folder.
	 * @return the folder
	 */
	private DomainFolder getSelectedFolder() {
		DomainFolder folder = treePanel.getSelectedDomainFolder();
		if (folder != null) {
			return folder;
		}

		DomainFile file = treePanel.getSelectedDomainFile();
		if (file != null) {
			return file.getParent();
		}

		return project.getProjectData().getRootFolder();
	}

	@Override
	public void actionPerformed(ActionEvent event) {
		int index = projectComboBox.getSelectedIndex();
		if (index < 0) {
			return;
		}

		ProjectLocator projectLocator = projectLocators[index];
		ProjectData pd = project.getProjectData(projectLocator);
		String projectName = projectLocator.getName();
		if (pd == null) {
			Msg.showError(this, getComponent(), "Error Getting Project Data",
				"Could not get project data for " + projectName);
		}
		else {
			treePanel.setProjectData(projectName, pd);
		}
	}

	/**
	 * Select a folder in the tree.
	 * @param folder the folder to select
	 */
	public void selectFolder(DomainFolder folder) {
		Swing.runLater(() -> treePanel.selectDomainFolder(folder));
	}

	/**
	 * Select the node that corresponds to the given domain file.
	 * @param file the file
	 */
	public void selectDomainFile(DomainFile file) {
		if (file != null) {
			Swing.runLater(() -> treePanel.selectDomainFile(file));
		}
	}

	@Override
	public void close() {
		super.close();
		removeWorkPanel();
		if (treePanel != null) {
			treePanel.dispose();
		}
		treePanel = null;
	}

	protected JPanel buildDataTreePanel() {

		JPanel panel = new JPanel();
		panel.setLayout(new BorderLayout());

		JPanel namePanel = createNamePanel();

		// data tree panel must be created before the combo box
		JPanel dataTreePanel = createDataTreePanel();

		// this allows users to press the OK button to choose the root folder
		treePanel.selectRootDataFolder();

		if (type == OPEN) {
			JPanel comboPanel = createComboBoxPanel();
			panel.add(comboPanel, BorderLayout.NORTH);
			populateProjectModel();
		}

		panel.add(dataTreePanel, BorderLayout.CENTER);
		panel.add(namePanel, BorderLayout.SOUTH);

		// can't add tree listeners until everything is built
		addTreeListeners();
		return panel;
	}

	@Override
	protected void okCallback() {
		cancelled = false;

		if (okActionListener == null) {
			close();
			return;
		}
		okActionListener.actionPerformed(new ActionEvent(okButton, 0, okButton.getActionCommand()));
	}

	public boolean wasCancelled() {
		return cancelled;
	}

	@Override
	protected void cancelCallback() {
		cancelled = true;
		close();
	}

	/**
	 * Create the data tree panel.
	 */
	private JPanel createDataTreePanel() {

		JPanel panel = new JPanel();
		panel.setLayout(new BorderLayout());
		ProjectData pd = project.getProjectData();

		treePanel = new ProjectDataTreePanel(project.getName(), true, // is for the active project
			null, filter);
		if (treeSelectionMode != null) {
			treePanel.getTreeSelectionModel().setSelectionMode(treeSelectionMode);
		}
		treePanel.setProjectData(project.getName(), pd);
		treePanel.addTreeSelectionListener(this);
		treePanel.setPreferredTreePanelSize(new Dimension(150, 150));

		// don't put the filter in the dialog when the user can/must type a name, as it's confusing
		boolean userChoosesName = (type == SAVE) || (type == CREATE);
		treePanel.setTreeFilterEnabled(!userChoosesName);

		panel.add(treePanel, BorderLayout.CENTER);
		return panel;
	}

	protected void addTreeListeners() {
		if (type == OPEN) {

			treePanel.addTreeMouseListener(new GMouseListenerAdapter() {
				@Override
				public void doubleClickTriggered(MouseEvent e) {
					if (okButton.isEnabled()) {
						okCallback();
					}
				}
			});
		}
	}

	/**
	 * Create the combo box panel that shows list of project names that
	 * are currently open, including the active project.
	 */
	private JPanel createComboBoxPanel() {

		JPanel panel = new JPanel();
		panel.setLayout(new BorderLayout());
		panel.setBorder(new TitledBorder("Current Projects"));
		projectComboBox = new GComboBox<>();
		DefaultComboBoxModel<String> model = new DefaultComboBoxModel<>();
		projectComboBox.setModel(model);
		model.addElement("defaultProject");
		panel.add(projectComboBox, BorderLayout.CENTER);
		projectComboBox.addActionListener(this);

		return panel;
	}

	private JPanel createNamePanel() {

		JPanel outerPanel = new JPanel();
		outerPanel.setLayout(new BorderLayout(5, 0));

		nameField = new JTextField(12);
		nameField.getDocument().addDocumentListener(new DocumentListener() {

			@Override
			public void removeUpdate(DocumentEvent e) {
				update();
			}

			private void update() {
				String text = nameField.getText();

				switch (type) {
					case OPEN:
						// handled by valueChanged()
						break;
					case SAVE:
						if (text == null || text.isEmpty()) {
							DomainFile file = treePanel.getSelectedDomainFile();
							okButton.setEnabled(file != null);
						}
						break;
					case CREATE:
						if (text == null || text.isEmpty()) {
							DomainFile file = treePanel.getSelectedDomainFile();
							okButton.setEnabled(file != null);
						}
						break;
					case CHOOSE_FOLDER:
						// handled by valueChanged()
						break;
					default:
						throw new AssertException("Must handle new type!: " + type);
				}

				setOkEnabled((text != null) && !text.isEmpty());
			}

			@Override
			public void insertUpdate(DocumentEvent e) {
				update();
			}

			@Override
			public void changedUpdate(DocumentEvent e) {
				update();
			}
		});

		boolean userChoosesName = (type == SAVE) || (type == CREATE);
		nameField.setEditable(userChoosesName);
		nameField.setEnabled(userChoosesName);

		JPanel namePanel = new JPanel(new PairLayout(2, 5, 100));

		if (!userChoosesName) {
			namePanel.setBorder(BorderFactory.createEmptyBorder(20, 5, 5, 5));
		}
		namePanel.add(new GLabel("Folder Path:", SwingConstants.RIGHT));

		folderNameLabel = new GDLabel("   ");
		namePanel.add(folderNameLabel);

		namePanel.add(
			new GLabel(type == CHOOSE_FOLDER ? "Folder Name:" : "Name:", SwingConstants.RIGHT));
		namePanel.add(nameField);

		outerPanel.add(namePanel, BorderLayout.CENTER);

		FieldKeyListener l = new FieldKeyListener();
		nameField.addKeyListener(l);
		nameField.addActionListener(e -> okCallback());

		return outerPanel;
	}

	private void populateProjectModel() {
		ProjectLocator[] views = project.getProjectViews();

		projectLocators = new ProjectLocator[views.length + 1];
		// make the current project the first in the list
		projectLocators[0] = project.getProjectLocator();
		for (int i = 0; i < views.length; i++) {
			projectLocators[i + 1] = views[i];
		}

		// populate the combo box
		DefaultComboBoxModel<String> model =
			(DefaultComboBoxModel<String>) projectComboBox.getModel();
		model.removeAllElements();

		Set<String> map = new HashSet<>();
		for (ProjectLocator projectLocator : projectLocators) {
			String name = projectLocator.getName();
			if (map.contains(name)) {
				model.addElement(name + " (" + projectLocator.getLocation() + ")");
			}
			else {
				map.add(name);
				model.addElement(name);
			}
		}
		map = null;
	}

	public void setSearchText(String s) {
		if (s != null) {
			treePanel.findAndSelect(s);
		}
	}

	private class FieldKeyListener extends KeyAdapter {
		@Override
		public void keyPressed(KeyEvent e) {
			clearStatusText();
		}
	}

	private class SetNameTextTask extends GTreeTask {

		private String text;

		SetNameTextTask(GTree gTree, String text) {
			super(gTree);
			this.text = text;
		}

		@Override
		public void run(TaskMonitor monitor) throws CancelledException {
			runOnSwingThread(() -> {
				nameField.setText(text.trim());
				nameField.selectAll();
			});
		}
	}
}
