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

import java.awt.Rectangle;
import java.awt.datatransfer.*;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.apache.commons.lang3.StringUtils;

import docking.ActionContext;
import docking.ComponentProvider;
import docking.dnd.GenericDataFlavor;
import docking.dnd.StringTransferable;
import docking.tool.ToolConstants;
import docking.widgets.fieldpanel.Layout;
import docking.widgets.fieldpanel.internal.*;
import generic.text.TextLayoutGraphics;
import ghidra.app.cmd.comments.CodeUnitInfoPasteCmd;
import ghidra.app.cmd.comments.SetCommentCmd;
import ghidra.app.cmd.function.SetVariableNameCmd;
import ghidra.app.cmd.label.RenameLabelCmd;
import ghidra.app.plugin.core.codebrowser.CodeViewerActionContext;
import ghidra.app.services.ClipboardContentProviderService;
import ghidra.app.util.*;
import ghidra.app.util.viewer.listingpanel.ListingModel;
import ghidra.framework.model.DomainFile;
import ghidra.framework.options.OptionsChangeListener;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.database.mem.AddressSourceInfo;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.mem.Memory;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.model.symbol.*;
import ghidra.program.util.*;
import ghidra.util.Msg;
import ghidra.util.bean.opteditor.OptionsVetoException;
import ghidra.util.task.CancellableIterator;
import ghidra.util.task.TaskMonitor;
import util.CollectionUtils;

public class CodeBrowserClipboardProvider extends ByteCopier
		implements ClipboardContentProviderService, OptionsChangeListener {

	protected static final PaintContext PAINT_CONTEXT = new PaintContext();
	private static int[] COMMENT_TYPESx = CommentTypes.getTypes();

	public static final ClipboardType ADDRESS_TEXT_TYPE =
		new ClipboardType(DataFlavor.stringFlavor, "Address");
	public static final ClipboardType ADDRESS_TEXT_WITH_OFFSET_TYPE =
		new ClipboardType(DataFlavor.stringFlavor, "Address w/ Offset");
	public static final ClipboardType BYTE_SOURCE_OFFSET_TYPE =
		new ClipboardType(DataFlavor.stringFlavor, "Byte Source Offset");
	public static final ClipboardType FUNCTION_OFFSET_TYPE =
		new ClipboardType(DataFlavor.stringFlavor, "Function Offset");
	public static final ClipboardType IMAGEBASE_OFFSET_TYPE =
		new ClipboardType(DataFlavor.stringFlavor, "Imagebase Offset");
	public static final ClipboardType BLOCK_OFFSET_TYPE =
		new ClipboardType(DataFlavor.stringFlavor, "Memory Block Offset");
	public static final ClipboardType CODE_TEXT_TYPE =
		new ClipboardType(DataFlavor.stringFlavor, "Formatted Code");
	public static final ClipboardType LABELS_COMMENTS_TYPE =
		new ClipboardType(CodeUnitInfoTransferable.localDataTypeFlavor, "Labels and Comments");
	public static final ClipboardType LABELS_TYPE =
		new ClipboardType(CodeUnitInfoTransferable.localDataTypeFlavor, "Labels");
	public static final ClipboardType COMMENTS_TYPE =
		new ClipboardType(CodeUnitInfoTransferable.localDataTypeFlavor, "Comments");
	public static final ClipboardType GHIDRA_LOCAL_URL_TYPE =
		new ClipboardType(DataFlavor.stringFlavor, "Local GhidraURL");
	public static final ClipboardType GHIDRA_SHARED_URL_TYPE =
		new ClipboardType(DataFlavor.stringFlavor, "Shared GhidraURL");

	public static final ClipboardType GHIDRA_DATA_TEXT_TYPE =
		new ClipboardType(DataFlavor.stringFlavor, "Data");
	public static final ClipboardType GHIDRA_DEREFERENCED_DATA_TEXT_TYPE =
		new ClipboardType(DataFlavor.stringFlavor, "Dereferenced Data");

	private static final List<ClipboardType> COPY_TYPES = createCopyTypesList();

	private static List<ClipboardType> createCopyTypesList() {
		List<ClipboardType> list = new LinkedList<>();

		list.add(CODE_TEXT_TYPE);
		list.add(LABELS_COMMENTS_TYPE);
		list.add(LABELS_TYPE);
		list.add(COMMENTS_TYPE);
		list.add(BYTE_STRING_TYPE);
		list.add(BYTE_STRING_NO_SPACE_TYPE);
		list.add(GHIDRA_DATA_TEXT_TYPE);
		list.add(GHIDRA_DEREFERENCED_DATA_TEXT_TYPE);
		list.add(PYTHON_BYTE_STRING_TYPE);
		list.add(PYTHON_LIST_TYPE);
		list.add(CPP_BYTE_ARRAY_TYPE);
		list.add(ADDRESS_TEXT_TYPE);
		list.add(ADDRESS_TEXT_WITH_OFFSET_TYPE);
		list.add(BYTE_SOURCE_OFFSET_TYPE);
		list.add(BLOCK_OFFSET_TYPE);
		list.add(FUNCTION_OFFSET_TYPE);
		list.add(IMAGEBASE_OFFSET_TYPE);

		return list;
	}

	protected boolean copyFromSelectionEnabled;
	protected ComponentProvider componentProvider;
	private ListingModel model;

	private Set<ChangeListener> listeners = new CopyOnWriteArraySet<>();
	private String stringContent;
	private boolean includeQuotesForStringData;

	public CodeBrowserClipboardProvider(PluginTool tool, ComponentProvider componentProvider) {
		this.tool = tool;
		this.componentProvider = componentProvider;

		PAINT_CONTEXT.setTextCopying(true);

		ToolOptions options = tool.getOptions(ToolConstants.TOOL_OPTIONS);
		includeQuotesForStringData =
			!options.getBoolean(ClipboardPlugin.REMOVE_QUOTES_OPTION, false);
		options.addOptionsChangeListener(this);
	}

	@Override
	public void addChangeListener(ChangeListener listener) {
		listeners.add(listener);
	}

	@Override
	public void removeChangeListener(ChangeListener listener) {
		listeners.remove(listener);
	}

	private void notifyStateChanged() {
		ChangeEvent event = new ChangeEvent(this);
		for (ChangeListener listener : listeners) {
			listener.stateChanged(event);
		}
	}

	@Override
	public Transferable copy(TaskMonitor monitor) {
		if (stringContent != null) {
			return createStringTransferable(stringContent);
		}

		if (copyFromSelectionEnabled) {
			return copyCode(monitor);
		}

		return copyFromCurrentLocation();
	}

	@Override
	public boolean paste(Transferable pasteData) {
		try {
			DataFlavor[] flavors = pasteData.getTransferDataFlavors();
			for (DataFlavor element : flavors) {
				if (element.equals(LABELS_COMMENTS_TYPE.getFlavor())) {
					return pasteLabelsComments(pasteData, true, true);
				}
				else if (element.equals(LABELS_TYPE.getFlavor())) {
					return pasteLabelsComments(pasteData, true, false);
				}
				else if (element.equals(COMMENTS_TYPE.getFlavor())) {
					return pasteLabelsComments(pasteData, false, true);
				}
				else if (element.equals(LabelStringTransferable.labelStringFlavor)) {
					return pasteLabelString(pasteData);
				}
				else if (element.equals(NonLabelStringTransferable.nonLabelStringFlavor)) {
					return pasteNonLabelString(pasteData);
				}
			}

			if (super.pasteBytes(pasteData)) {
				return true;
			}

			tool.setStatusInfo("Paste failed: unsupported data type", true);
		}
		catch (Exception e) {
			String msg = e.getMessage();
			if (msg == null) {
				msg = e.toString();
			}

			Msg.error(this, "Unexpected Exception: " + msg, e);
			tool.setStatusInfo("Paste failed: " + msg, true);
		}
		return false;
	}

	@Override
	public List<ClipboardType> getCurrentCopyTypes() {
		ClipboardType urlType = getGhidraUrlClipboardType();
		if (urlType == null) {
			return COPY_TYPES;
		}

		List<ClipboardType> currentCopyTypes = new ArrayList<>(COPY_TYPES);
		currentCopyTypes.add(urlType);
		return currentCopyTypes;
	}

	private ClipboardType getGhidraUrlClipboardType() {
		if (currentProgram == null) {
			return null;
		}

		DomainFile domainFile = currentProgram.getDomainFile();
		if (domainFile == null) {
			return null;
		}
		if (domainFile.getLocalProjectURL(null) != null) {
			return GHIDRA_LOCAL_URL_TYPE;
		}
		if (domainFile.getSharedProjectURL(null) != null) {
			return GHIDRA_SHARED_URL_TYPE;
		}
		return null;
	}

	@Override
	public Transferable copySpecial(ClipboardType copyType, TaskMonitor monitor) {

		if (copyType == ADDRESS_TEXT_TYPE) {
			return copyAddress(monitor);
		}
		else if (copyType == ADDRESS_TEXT_WITH_OFFSET_TYPE) {
			return copySymbolString(monitor);
		}
		else if (copyType == BYTE_SOURCE_OFFSET_TYPE) {
			return copyByteSourceOffset(monitor);
		}
		else if (copyType == BLOCK_OFFSET_TYPE) {
			return copyBlockSourceOffset(monitor);
		}
		else if (copyType == FUNCTION_OFFSET_TYPE) {
			return copyFunctionSourceOffset(monitor);
		}
		else if (copyType == IMAGEBASE_OFFSET_TYPE) {
			return copyImagebaseSourceOffset(monitor);
		}
		else if (copyType == CODE_TEXT_TYPE) {
			return copyCode(monitor);
		}
		else if (copyType == LABELS_COMMENTS_TYPE) {
			return copyLabelsComments(true, true, monitor);
		}
		else if (copyType == LABELS_TYPE) {
			return copyLabelsComments(true, false, monitor);
		}
		else if (copyType == COMMENTS_TYPE) {
			return copyLabelsComments(false, true, monitor);
		}
		else if (copyType == GHIDRA_LOCAL_URL_TYPE) {
			return copyLocalGhidraURL();
		}
		else if (copyType == GHIDRA_SHARED_URL_TYPE) {
			return copySharedGhidraURL();
		}
		else if (copyType == GHIDRA_DATA_TEXT_TYPE) {
			return copyDataText();
		}
		else if (copyType == GHIDRA_DEREFERENCED_DATA_TEXT_TYPE) {
			return copyReferencedDataText();
		}

		return copyBytes(copyType, monitor);
	}

	public void setStringContent(String text) {
		this.stringContent = text;
	}

	public String getStringContent() {
		return stringContent;
	}

	public void setLocation(ProgramLocation location) {
		currentLocation = location;
	}

	public void setSelection(ProgramSelection selection) {
		currentSelection = selection;
		copyFromSelectionEnabled = selection != null && !selection.isEmpty();
		notifyStateChanged();
	}

	public void setProgram(Program p) {
		currentProgram = p;
		currentLocation = null;
		currentSelection = null;
	}

	public void setListingLayoutModel(ListingModel model) {
		this.model = model;
	}

	protected ListingModel getListingModel() {
		return model;
	}

	private Transferable copyFromCurrentLocation() {

		Address address = currentLocation.getAddress();
		if (currentLocation instanceof AddressFieldLocation) {
			return new NonLabelStringTransferable(address.toString());
		}
		else if (currentLocation instanceof LabelFieldLocation) {
			LabelFieldLocation labelFieldLocation = (LabelFieldLocation) currentLocation;
			return new LabelStringTransferable(labelFieldLocation.getName());
		}
		else if (currentLocation instanceof FunctionNameFieldLocation) {
			FunctionNameFieldLocation functionNameLocation =
				(FunctionNameFieldLocation) currentLocation;
			return new LabelStringTransferable(functionNameLocation.getFunctionName());
		}
		else if (currentLocation instanceof CommentFieldLocation) {
			CommentFieldLocation commentFieldLocation = (CommentFieldLocation) currentLocation;
			String[] comment = commentFieldLocation.getComment();
			return new NonLabelStringTransferable(comment);
		}
		else if (currentLocation instanceof BytesFieldLocation) {
			// bytes are special--let them get copied and pasted as normal
			return copyByteString(address);
		}
		else if (currentLocation instanceof OperandFieldLocation) {
			return getOperandLocationTransferable((OperandFieldLocation) currentLocation);
		}
		else if (currentLocation instanceof MnemonicFieldLocation) {
			MnemonicFieldLocation location = (MnemonicFieldLocation) currentLocation;
			return new NonLabelStringTransferable(location.getMnemonic());
		}
		else if (currentLocation instanceof VariableLocation) {
			VariableLocation variableLocation = (VariableLocation) currentLocation;
			Variable variable = variableLocation.getVariable();
			return new LabelStringTransferable(variable.getName());
		}

		return null;
	}

	private Transferable getOperandLocationTransferable(OperandFieldLocation location) {

		int opIndex = location.getOperandIndex();
		Listing listing = currentProgram.getListing();
		Instruction instruction = listing.getInstructionAt(location.getAddress());
		if (instruction == null) {
			// just copy the representation--WYSIWIG
			return new NonLabelStringTransferable(location.getOperandRepresentation());
		}

		Reference reference = instruction.getPrimaryReference(opIndex);
		if (reference == null) {
			// just copy the representation--WYSIWIG
			return new NonLabelStringTransferable(location.getOperandRepresentation());
		}

		Variable variable = currentProgram.getReferenceManager().getReferencedVariable(reference);
		if (variable != null) {
			return new LabelStringTransferable(variable.getName());
		}

		SymbolTable symbolTable = currentProgram.getSymbolTable();
		Symbol symbol = symbolTable.getSymbol(reference);
		if (symbol != null) {
			return new LabelStringTransferable(symbol.getName());
		}

		// just copy the representation--WYSIWIG
		return new NonLabelStringTransferable(location.getOperandRepresentation());
	}

	private Transferable copyAddress(TaskMonitor monitor) {
		AddressSetView addrs = getSelectedAddresses();
		Iterable<Address> iterable = addrs.getAddresses(true);
		CancellableIterator<Address> it = new CancellableIterator<>(iterable.iterator(), monitor);
		return createStringTransferable(StringUtils.join(it, "\n"));
	}

	private Transferable copySymbolString(TaskMonitor monitor) {
		Listing listing = currentProgram.getListing();
		List<String> strings = new ArrayList<>();
		CodeUnitIterator codeUnits = listing.getCodeUnits(getSelectedAddresses(), true);
		while (codeUnits.hasNext() && !monitor.isCancelled()) {
			CodeUnit cu = codeUnits.next();
			Address addr = cu.getAddress();
			Function function = listing.getFunctionContaining(addr);
			if (function == null) {
				strings.add(addr.toString());
				continue;
			}

			String name = function.getName(true);
			Address entry = function.getEntryPoint();
			int delta = addr.compareTo(entry);
			if (delta == 0) {
				strings.add(name);
			}
			else if (delta > 0) {
				strings.add(String.format("%s + %#x", name, addr.subtract(entry)));
			}
			else {
				strings.add(String.format("%s - %#x", name, entry.subtract(addr)));
			}
		}
		return createStringTransferable(StringUtils.join(strings, "\n"));
	}

	private Transferable copyByteSourceOffset(TaskMonitor monitor) {
		AddressSetView addrs = getSelectedAddresses();
		Memory currentMemory = currentProgram.getMemory();
		List<String> strings = new ArrayList<>();
		AddressIterator addresses = addrs.getAddresses(true);
		while (addresses.hasNext() && !monitor.isCancelled()) {
			AddressSourceInfo addressSourceInfo =
				currentMemory.getAddressSourceInfo(addresses.next());
			if (addressSourceInfo != null) {
				long fileOffset = addressSourceInfo.getFileOffset();
				String fileOffsetString =
					fileOffset != -1 ? "%x".formatted(fileOffset) : "<NO_OFFSET>";
				strings.add(fileOffsetString);
			}
		}
		return createStringTransferable(String.join("\n", strings));
	}

	private Transferable copyBlockSourceOffset(TaskMonitor monitor) {
		AddressSetView addrs = getSelectedAddresses();
		Memory mem = currentProgram.getMemory();
		List<String> strings = new ArrayList<>();
		AddressIterator addresses = addrs.getAddresses(true);
		while (addresses.hasNext() && !monitor.isCancelled()) {
			Address addr = addresses.next();
			MemoryBlock block = mem.getBlock(addr);
			strings.add(
				block != null ? "%x".formatted(addr.subtract(block.getStart())) : "<NO_OFFSET>");
		}
		return createStringTransferable(String.join("\n", strings));
	}

	private Transferable copyFunctionSourceOffset(TaskMonitor monitor) {
		AddressSetView addrs = getSelectedAddresses();
		Listing listing = currentProgram.getListing();
		List<String> strings = new ArrayList<>();
		AddressIterator addresses = addrs.getAddresses(true);
		while (addresses.hasNext() && !monitor.isCancelled()) {
			Address addr = addresses.next();
			Function function = listing.getFunctionContaining(addr);
			strings.add(function != null ? "%x".formatted(addr.subtract(function.getEntryPoint()))
					: "<NO_OFFSET>");
		}
		return createStringTransferable(String.join("\n", strings));
	}

	private Transferable copyImagebaseSourceOffset(TaskMonitor monitor) {
		AddressSetView addrs = getSelectedAddresses();
		List<String> strings = new ArrayList<>();
		AddressIterator addresses = addrs.getAddresses(true);
		while (addresses.hasNext() && !monitor.isCancelled()) {
			Address addr = addresses.next();
			Address imagebase = currentProgram.getImageBase();
			strings.add(
				addr.hasSameAddressSpace(imagebase) ? "%x".formatted(addr.subtract(imagebase))
						: "<NO_OFFSET>");
		}
		return createStringTransferable(String.join("\n", strings));
	}

	protected Transferable copyCode(TaskMonitor monitor) {

		AddressSetView addressSet = getSelectedAddresses();
		ListingModel listingModel = getListingModel();
		TextLayoutGraphics g = new TextLayoutGraphics();
		LayoutBackgroundColorManager colorMap =
			new EmptyLayoutBackgroundColorManager(PAINT_CONTEXT.getBackground());
		Rectangle rect = new Rectangle(Integer.MAX_VALUE, Integer.MAX_VALUE);
		AddressRangeIterator ranges = addressSet.getAddressRanges();
		while (ranges.hasNext() && !monitor.isCancelled()) {
			AddressRange curRange = ranges.next();
			Address address = curRange.getMinAddress();
			Address maxAddress = curRange.getMaxAddress();

			while (address != null && address.compareTo(maxAddress) <= 0) {
				if (monitor.isCancelled()) {
					break;
				}

				Layout layout = listingModel.getLayout(address, false);
				if (layout != null) {
					layout.paint(null, g, PAINT_CONTEXT, rect, colorMap, null);
					g.flush();
				}
				address = listingModel.getAddressAfter(address);
			}
		}

		return createStringTransferable(g.getBuffer());
	}

	private Transferable copyByteString(Address address) {
		AddressSet set = new AddressSet(address);
		return createStringTransferable(copyBytesAsString(set, false, TaskMonitor.DUMMY));
	}

	private CodeUnitInfoTransferable copyLabelsComments(boolean copyLabels, boolean copyComments,
			TaskMonitor monitor) {
		AddressSetView addressSet = getSelectedAddresses();
		List<CodeUnitInfo> list = new ArrayList<>();
		Address startAddr = addressSet.getMinAddress();
		getCodeUnitInfo(addressSet, startAddr, list, copyLabels, copyComments, monitor);
		return new CodeUnitInfoTransferable(list);
	}

	/**
	 * Gets the Local GhidraURL to the currentLocation within the currentProgram.
	 * @return string transferable GhidraURL type.
	 */
	private Transferable copyLocalGhidraURL() {
		String address = currentLocation.getAddress().toString();
		URL url = currentProgram.getDomainFile().getLocalProjectURL(address);
		return createStringTransferable(url.toString());
	}

	/**
	 * Gets the Shared GhidraURL to the currentLocation within the currentProgram.
	 * @return string transferable GhidraURL type.
	 */
	private Transferable copySharedGhidraURL() {
		String address = currentLocation.getAddress().toString();
		URL url = currentProgram.getDomainFile().getSharedProjectURL(address);
		return createStringTransferable(url.toString());
	}

	private Transferable copyDataText() {
		return copyDataText(false);
	}

	private Transferable copyReferencedDataText() {
		return copyDataText(true);
	}

	private Transferable copyDataText(boolean followPointers) {
		AddressSetView addrs = getSelectedAddresses();
		List<String> strings = new ArrayList<>();
		Listing listing = currentProgram.getListing();
		if (addrs instanceof ProgramSelection programSelection &&
			programSelection.getInteriorSelection() != null) {
			InteriorSelection interiorSelection = programSelection.getInteriorSelection();
			copyInteriorDataText(strings, listing, interiorSelection, followPointers);
		}
		else {
			for (Data data : listing.getData(addrs, true)) {
				String string = getString(data, followPointers);
				if (string != null) {
					strings.add(string);
				}
			}
		}
		return createStringTransferable(String.join("\n", strings));
	}

	private void copyInteriorDataText(List<String> strings, Listing listing,
			InteriorSelection interior, boolean followPointers) {

		Data outer = listing.getDataContaining(interior.getStartAddress());
		ProgramLocation from = interior.getFrom();
		ProgramLocation to = interior.getTo();
		Data first = outer.getComponent(from.getComponentPath());
		Data last = outer.getComponent(to.getComponentPath());
		if (first == null || last == null) {
			return;
		}

		// We have a selection inside a data that has child elements. This may be several levels
		// deep. The first and last data retrieved above are children in the same parent. So
		// get the index of the first and last data in that parent and add string data for each
		// selected element in that parent.
		Data parent = first.getParent();
		int startIndex = first.getComponentIndex();
		int endIndex = last.getComponentIndex();
		for (int i = startIndex; i <= endIndex; i++) {
			Data child = parent.getComponent(i);
			String string = getString(child, followPointers);
			if (string != null) {
				strings.add(string);
			}
		}
	}

	private Data getReferencedData(Data data, Set<Address> visited) {
		Object value = data.getValue();
		if (!(value instanceof Address address)) {
			return null;
		}
		if (visited.contains(address)) {
			return null;
		}
		visited.add(address);
		Data referenced = currentProgram.getListing().getDataAt(address);
		if (referenced == null) {
			return null;
		}
		if (referenced.isPointer()) {
			return getReferencedData(referenced, visited);
		}
		return referenced;
	}

	private String getString(Data data, boolean followPointers) {
		if (!followPointers) {
			return getString(data);
		}
		// if following pointers and data is not a pointer, then nothing to do
		if (!data.isPointer()) {
			return null;
		}

		// otherwise follow pointers to non-pointer data and return its string
		Set<Address> visited = new HashSet<>();
		Data referenced = getReferencedData(data, visited);
		return getString(referenced);
	}

	private String getString(Data data) {
		// For now, don't get string data for compound object such as structures, unions, or arrays
		if (data == null || data.getNumComponents() > 0) {
			return null;
		}
		Object value = data.getValue();
		if (value instanceof String s) {
			if (includeQuotesForStringData) {
				return "\"" + s + "\"";
			}
			return s;
		}
		return data.getDefaultValueRepresentation();
	}

	private boolean pasteLabelsComments(Transferable pasteData, boolean pasteLabels,
			boolean pasteComments) {
		try {
			List<?> list =
				(List<?>) pasteData.getTransferData(CodeUnitInfoTransferable.localDataTypeFlavor);
			List<CodeUnitInfo> infos = CollectionUtils.asList(list, CodeUnitInfo.class);
			CodeUnitInfoPasteCmd cmd = new CodeUnitInfoPasteCmd(currentLocation.getAddress(), infos,
				pasteLabels, pasteComments);
			return tool.execute(cmd, currentProgram);
		}
		catch (Exception e) {
			String msg = e.getMessage();
			if (msg == null) {
				msg = e.toString();
			}
			tool.setStatusInfo("Paste failed: " + msg, true);
		}

		return false;
	}

	private boolean pasteLabelString(Transferable pasteData)
			throws UnsupportedFlavorException, IOException {

		String labelName =
			(String) pasteData.getTransferData(LabelStringTransferable.labelStringFlavor);
		Address address = currentLocation.getAddress();
		if (currentLocation instanceof LabelFieldLocation) {
			LabelFieldLocation labelFieldLocation = (LabelFieldLocation) currentLocation;
			String oldName = labelFieldLocation.getName();
			RenameLabelCmd cmd =
				new RenameLabelCmd(address, oldName, labelName, SourceType.USER_DEFINED);
			return tool.execute(cmd, currentProgram);
		}
		else if (currentLocation instanceof FunctionNameFieldLocation) {
			FunctionNameFieldLocation functionNameLocation =
				(FunctionNameFieldLocation) currentLocation;
			String oldName = functionNameLocation.getFunctionName();
			RenameLabelCmd cmd =
				new RenameLabelCmd(address, oldName, labelName, SourceType.USER_DEFINED);
			return tool.execute(cmd, currentProgram);
		}
		else if (currentLocation instanceof OperandFieldLocation) {
			return pasteOperandField((OperandFieldLocation) currentLocation, labelName);
		}

		// try pasting onto something that is not a label
		return maybePasteNonLabelString(labelName);
	}

	private boolean pasteOperandField(OperandFieldLocation operandLocation, String labelName) {

		int opIndex = operandLocation.getOperandIndex();
		Listing listing = currentProgram.getListing();
		Instruction instruction = listing.getInstructionAt(operandLocation.getAddress());
		if (instruction == null) {
			return false;
		}

		Reference reference = instruction.getPrimaryReference(opIndex);
		if (reference == null) {
			return false;
		}

		Variable var = currentProgram.getReferenceManager().getReferencedVariable(reference);
		if (var != null) {
			SetVariableNameCmd cmd =
				new SetVariableNameCmd(var, labelName, SourceType.USER_DEFINED);
			return tool.execute(cmd, currentProgram);
		}

		SymbolTable symbolTable = currentProgram.getSymbolTable();
		Symbol symbol = symbolTable.getSymbol(reference);
		if (symbol == null) {
			return false;
		}
		SymbolType type = symbol.getSymbolType();
		if ((type == SymbolType.LABEL) || (type == SymbolType.FUNCTION)) {
			RenameLabelCmd cmd = new RenameLabelCmd(symbol, labelName, SourceType.USER_DEFINED);
			return tool.execute(cmd, currentProgram);
		}

		// try pasting onto something that is not a label
		return maybePasteNonLabelString(labelName);
	}

	private boolean pasteNonLabelString(Transferable pasteData)
			throws UnsupportedFlavorException, IOException {

		String text =
			(String) pasteData.getTransferData(NonLabelStringTransferable.nonLabelStringFlavor);
		return maybePasteNonLabelString(text);
	}

	private boolean maybePasteNonLabelString(String string) {
		if (currentLocation instanceof CommentFieldLocation) {
			CommentFieldLocation commentFieldLocation = (CommentFieldLocation) currentLocation;
			Address address = commentFieldLocation.getAddress();
			CommentType commentType = commentFieldLocation.getCommentType();
			if (commentType != null) {
				SetCommentCmd cmd = new SetCommentCmd(address, commentType, string);
				return tool.execute(cmd, currentProgram);
			}
		}
		return false;
	}

	private void getCodeUnitInfo(AddressSetView set, Address startAddr, List<CodeUnitInfo> list,
			boolean copyLabels, boolean copyComments, TaskMonitor monitor) {
		Map<Address, CodeUnitInfo> map = new HashMap<>();
		if (copyLabels) {
			getFunctions(startAddr, set, list, map, monitor);
			getLabels(startAddr, set, list, map, monitor);
		}
		if (copyComments) {
			getComments(startAddr, set, list, map, monitor);
		}
	}

	private void getFunctions(Address startAddr, AddressSetView set, List<CodeUnitInfo> list,
			Map<Address, CodeUnitInfo> map, TaskMonitor monitor) {

		FunctionIterator it = currentProgram.getListing().getFunctions(set, true);
		while (it.hasNext() && !monitor.isCancelled()) {
			Function function = it.next();
			Address entry = function.getEntryPoint();
			CodeUnitInfo info = getInfoFromMap(list, map, entry, startAddr);
			info.setFunction(function);
		}
	}

	private void getComments(Address startAddr, AddressSetView set, List<CodeUnitInfo> list,
			Map<Address, CodeUnitInfo> map, TaskMonitor monitor) {

		Listing listing = currentProgram.getListing();
		CodeUnitIterator it = listing.getCodeUnitIterator(CodeUnit.COMMENT_PROPERTY, set, true);
		while (it.hasNext() && !monitor.isCancelled()) {
			CodeUnit cu = it.next();
			Address minAddress = cu.getMinAddress();
			CodeUnitInfo info = getInfoFromMap(list, map, minAddress, startAddr);
			setCommentInfo(cu, info);
		}
	}

	private void setCommentInfo(CodeUnit cu, CodeUnitInfo info) {
		for (CommentType type : CommentType.values()) {
			String[] comments = cu.getCommentAsArray(type);
			if (comments != null && comments.length > 0) {
				info.setComment(type, comments);
			}
		}
	}

	private void getLabels(Address startAddr, AddressSetView set, List<CodeUnitInfo> list,
			Map<Address, CodeUnitInfo> map, TaskMonitor monitor) {

		SymbolIterator it = currentProgram.getSymbolTable().getPrimarySymbolIterator(set, true);
		while (it.hasNext() && !monitor.isCancelled()) {
			Symbol symbol = it.next();
			Address minAddress = symbol.getAddress();
			Symbol[] symbols = currentProgram.getSymbolTable().getSymbols(minAddress);
			CodeUnitInfo info = getInfoFromMap(list, map, minAddress, startAddr);
			info.setSymbols(symbols);
		}
	}

	private CodeUnitInfo getInfoFromMap(List<CodeUnitInfo> list, Map<Address, CodeUnitInfo> map,
			Address minAddress, Address startAddr) {
		CodeUnitInfo info = map.get(minAddress);
		if (info == null) {
			long index = minAddress.subtract(startAddr);

			info = new CodeUnitInfo((int) index);
			map.put(minAddress, info);
			list.add(info);
		}

		return info;
	}

	@Override
	public boolean isValidContext(ActionContext context) {
		if (!(context instanceof CodeViewerActionContext)) {
			return false;
		}
		return context.getComponentProvider() == componentProvider;
	}

	@Override
	public ComponentProvider getComponentProvider() {
		return componentProvider;
	}

	@Override
	public boolean enableCopy() {
		return true;
	}

	@Override
	public boolean enableCopySpecial() {
		return true;
	}

	@Override
	public boolean canCopy() {
		return copyFromSelectionEnabled || stringContent != null ||
			canCopyCurrentLocationWithNoSelection();
	}

	@Override
	public boolean canCopySpecial() {
		return currentLocation != null;
	}

	private boolean canCopyCurrentLocationWithNoSelection() {

		if (currentLocation instanceof AddressFieldLocation) {
			return true;
		}
		else if (currentLocation instanceof LabelFieldLocation) {
			return true;
		}
		else if (currentLocation instanceof FunctionNameFieldLocation) {
			return true;
		}
		else if (currentLocation instanceof CommentFieldLocation) {
			return true;
		}
		else if (currentLocation instanceof BytesFieldLocation) {
			return true;
		}
		else if (currentLocation instanceof OperandFieldLocation) {
			return true;
		}
		else if (currentLocation instanceof MnemonicFieldLocation) {
			return true;
		}
		else if (currentLocation instanceof VariableLocation) {
			return true;
		}

		return false;
	}

	@Override
	public boolean enablePaste() {
		return true;
	}

	@Override
	public boolean canPaste(DataFlavor[] availableFlavors) {
		if (availableFlavors != null) {
			for (DataFlavor flavor : availableFlavors) {
				if (flavor.equals(LABELS_COMMENTS_TYPE.getFlavor()) ||
					flavor.equals(LABELS_TYPE.getFlavor()) ||
					flavor.equals(COMMENTS_TYPE.getFlavor()) ||
					flavor.equals(BYTE_STRING_TYPE.getFlavor()) ||
					flavor.equals(LabelStringTransferable.labelStringFlavor) ||
					flavor.equals(NonLabelStringTransferable.nonLabelStringFlavor)) {
					return true;
				}
				if (flavor.equals(DataFlavor.stringFlavor)) {
					return true; // check if it is a valid hex string?
				}
			}
		}
		return false;
	}

//==================================================================================================
// Unsupported Operations
//==================================================================================================

	@Override
	public void lostOwnership(Transferable transferable) {
		// unsupported
	}

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

	private static class LabelStringTransferable extends StringTransferable {

		public static final DataFlavor labelStringFlavor = new GenericDataFlavor(
			DataFlavor.javaJVMLocalObjectMimeType + "; class=java.lang.String",
			"Local label as string object");

		private final DataFlavor[] flavors = { labelStringFlavor, DataFlavor.stringFlavor };
		private final List<DataFlavor> flavorList = Arrays.asList(flavors);

		LabelStringTransferable(String name) {
			super(name);
		}

		@Override
		public Object getTransferData(DataFlavor flavor)
				throws UnsupportedFlavorException, IOException {
			if (flavor.equals(labelStringFlavor)) {
				return data;
			}
			if (flavor.equals(DataFlavor.stringFlavor)) {
				return data;
			}
			throw new UnsupportedFlavorException(flavor);
		}

		@Override
		public DataFlavor[] getTransferDataFlavors() {
			return flavors;
		}

		@Override
		public boolean isDataFlavorSupported(DataFlavor flavor) {
			return flavorList.contains(flavor);
		}
	}

	private static class NonLabelStringTransferable extends StringTransferable {

		public static final DataFlavor nonLabelStringFlavor = new GenericDataFlavor(
			DataFlavor.javaJVMLocalObjectMimeType + "; class=java.lang.String",
			"Local non-label as string object");

		private final DataFlavor[] flavors = { nonLabelStringFlavor, DataFlavor.stringFlavor };
		private final List<DataFlavor> flavorList = Arrays.asList(flavors);

		NonLabelStringTransferable(String[] text) {
			super(combine(text));
		}

		private static String combine(String[] text) {
			StringBuilder buildy = new StringBuilder();
			for (String string : text) {
				if (buildy.length() > 0) {
					buildy.append('\n');
				}
				buildy.append(string);
			}
			return buildy.toString();
		}

		NonLabelStringTransferable(String text) {
			super(text);
		}

		@Override
		public Object getTransferData(DataFlavor flavor)
				throws UnsupportedFlavorException, IOException {
			if (flavor.equals(nonLabelStringFlavor)) {
				return data;
			}
			if (flavor.equals(DataFlavor.stringFlavor)) {
				return data;
			}
			throw new UnsupportedFlavorException(flavor);
		}

		@Override
		public DataFlavor[] getTransferDataFlavors() {
			return flavors;
		}

		@Override
		public boolean isDataFlavorSupported(DataFlavor flavor) {
			return flavorList.contains(flavor);
		}
	}

	@Override
	public void optionsChanged(ToolOptions options, String optionName, Object oldValue,
			Object newValue) throws OptionsVetoException {
		if (optionName.equals(ClipboardPlugin.REMOVE_QUOTES_OPTION)) {
			includeQuotesForStringData =
				!options.getBoolean(ClipboardPlugin.REMOVE_QUOTES_OPTION, false);
		}
	}
}
