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

import java.awt.Color;
import java.util.ArrayList;
import java.util.List;

import javax.swing.event.ChangeEvent;

import docking.widgets.fieldpanel.support.Highlight;
import ghidra.app.plugin.core.navigation.FunctionUtils;
import ghidra.app.util.viewer.field.*;
import ghidra.framework.model.*;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.Structure;
import ghidra.program.model.listing.*;
import ghidra.program.util.*;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;

/**
 * A location descriptor that should be extended by location descriptor implementations that are
 * based upon data types.
 */
abstract class DataTypeLocationDescriptor extends LocationDescriptor {

	protected DataType originalDataType; // the one passed in at construction time
	protected DataType baseDataType; // the one used to find references (could be a base type)
	protected String dataTypeName;   // e.g., Foo or Foo.bar.baz

	DataTypeLocationDescriptor(ProgramLocation location, Program program) {
		super(location, program);

		if (location == null) {
			throw new NullPointerException(
				"Cannot create a LocationDescriptor from a null ProgramLocation");
		}

		originalDataType = getSourceDataType();
		homeAddress = location.getAddress();
		baseDataType = loadDataType();
		label = generateLabel();
		dataTypeName = getDataTypeName();
	}

	@Override
	public String getTypeName() {
		return dataTypeName;
	}

	@Override
	protected void doGetReferences(Accumulator<LocationReference> accumulator, TaskMonitor monitor)
			throws CancelledException {
		findDataTypeReferences(accumulator, monitor);
	}

	/**
	 * The original data type that this location descriptor describes
	 * @return the type
	 */
	protected abstract DataType getSourceDataType();

	/**
	 * Generates the label for the results window
	 * @return the label
	 */
	protected abstract String generateLabel();

	/**
	 * Returns the name of the data type, for example, 'Foo' or 'Foo.bar.baz'
	 * @return the name
	 */
	protected abstract String getDataTypeName();

	/**
	 * The base data type that this location descriptor describes (this may be the same as the
	 * original data type.
	 * @return the type.
	 */
	protected DataType getBaseDataType() {
		return getSourceDataType(); // by default these two values are the same
	}

	private void findDataTypeReferences(Accumulator<LocationReference> accumulator,
			TaskMonitor monitor) throws CancelledException {

		DataType currentDataType = getDataType();
		ReferenceUtils.findDataTypeReferences(accumulator, currentDataType, program,
			useDynamicSearching, monitor);
	}

	private DataType loadDataType() {
		if (baseDataType == null) {
			baseDataType = getBaseDataType();
		}
		return baseDataType;
	}

	protected DataType getDataType() {
		if (baseDataType instanceof Structure) {
			Data data = getData(getLocation());
			if (data != null) {
				return ReferenceUtils.getBaseDataType(data.getDataType());
			}
		}

		return ReferenceUtils.getBaseDataType(baseDataType);
	}

	protected Data getData(ProgramLocation location) {
		Listing listing = program.getListing();
		Address address = location.getAddress();
		Data data = listing.getDataContaining(address);
		if (data != null) {
			return data.getComponent(location.getComponentPath());
		}

		return null;
	}

	@Override
	protected boolean domainObjectChanged(DomainObjectChangedEvent changeEvent) {

		for (int i = 0; i < changeEvent.numRecords(); i++) {
			DomainObjectChangeRecord domainObjectRecord = changeEvent.getChangeRecord(i);
//			int eventType = domainObjectRecord.getEventType();
			EventType eventType = domainObjectRecord.getEventType();
			if (eventType == DomainObjectEvent.RESTORED) {
				return checkForAddressChange(domainObjectRecord);
			}
			if (eventType instanceof ProgramEvent type) {
				switch (type) {
					case FUNCTION_CHANGED:
						ProgramChangeRecord changeRecord = (ProgramChangeRecord) domainObjectRecord;
						Address functionAddress = changeRecord.getStart();
						if (referencesContain(functionAddress) &&
							functionContainsDataType(functionAddress)) {
							return checkForAddressChange(changeRecord);
						}
						break;

					case MEMORY_BLOCK_MOVED:
					case MEMORY_BLOCK_REMOVED:
					case SYMBOL_REMOVED:
					case REFERENCE_REMOVED:
					case CODE_REMOVED:
					case FUNCTION_REMOVED:
					case VARIABLE_REFERENCE_REMOVED:
						return checkForAddressChange(domainObjectRecord);
					case CODE_ADDED:
					case MEMORY_BLOCK_ADDED:
					case SYMBOL_ADDED:
					case REFERENCE_ADDED:
					case FUNCTION_ADDED:
					case VARIABLE_REFERENCE_ADDED:
					case DATA_TYPE_RENAMED:
					case DATA_TYPE_REPLACED:
						// signal that the reference addresses may be out-of-date
						if (modelFreshnessListener != null) {
							modelFreshnessListener.stateChanged(new ChangeEvent(this));
						}
						return true;
					default:
				}
			}
		}
		return false;
	}

	private boolean functionContainsDataType(Address functionAddress) {
		FunctionManager functionManager = program.getFunctionManager();
		Function function = functionManager.getFunctionAt(functionAddress);
		DataType currentDataType = getDataType();
		if (function != null) {
			List<Variable> allVariables = ReferenceUtils.getVariables(function, true);
			for (Variable variable : allVariables) {
				DataType variableDataType = variable.getDataType();
				if (ReferenceUtils.getBaseDataType(variableDataType)
						.isEquivalent(currentDataType)) {
					return true;
				}
			}

			DataType returnType = function.getReturnType();
			if (ReferenceUtils.getBaseDataType(returnType).isEquivalent(currentDataType)) {
				return true;
			}
		}

		return false;
	}

	@Override
	Highlight[] getHighlights(String text, Object object,
			Class<? extends FieldFactory> fieldFactoryClass, Color highlightColor) {

		Address currentAddress = getAddressForHighlightObject(object);
		if (!isInAddresses(currentAddress)) {
			return EMPTY_HIGHLIGHTS;
		}

		if (MnemonicFieldFactory.class.isAssignableFrom(fieldFactoryClass) &&
			(object instanceof Data)) {
			// compare against the underlying datatype, since the display text is different
			Data data = (Data) object;
			DataType otherBaseDataType = ReferenceUtils.getBaseDataType(data.getDataType());
			if (otherBaseDataType.isEquivalent(baseDataType)) {
				Highlight[] dtHighlights = getMnemonicDataTypeHighlights(text, highlightColor);
				return dtHighlights;
			}
		}
		else if (MnemonicFieldFactory.class.isAssignableFrom(fieldFactoryClass) ||
			OperandFieldFactory.class.isAssignableFrom(fieldFactoryClass) ||
			VariableTypeFieldFactory.class.isAssignableFrom(fieldFactoryClass)) {

			String highlightText = getHighlightStringForDataTypeName(text);
			if (highlightText != null) {
				return new Highlight[] {
					new Highlight(0, highlightText.length() - 1, highlightColor) };
			}
		}
		else if (FunctionSignatureFieldFactory.class.isAssignableFrom(fieldFactoryClass)) {
			// pull out the matching pieces of the data type
			List<Highlight> list = new ArrayList<>();

			Function function = (Function) object;
			FieldStringInfo returnTypeStringInfo =
				FunctionUtils.getFunctionReturnTypeStringInfo(function, text);
			String returnTypeString = returnTypeStringInfo.getFieldString();
			if (label.equals(returnTypeString)) {
				int offset = returnTypeStringInfo.getOffset();
				list.add(
					new Highlight(offset, offset + returnTypeString.length() - 1, highlightColor));
			}

			FieldStringInfo[] parameterStringInfos =
				FunctionUtils.getFunctionParameterStringInfos(function, text);
			for (FieldStringInfo info : parameterStringInfos) {
				String paramString = info.getFieldString();
				String highlightText = getHighlightStringForParameterDeclaration(paramString);
				if (highlightText != null) {
					int offset = info.getOffset();
					int length = offset + highlightText.length() - 1;
					list.add(new Highlight(offset, length, highlightColor));
				}
			}

			return list.toArray(new Highlight[list.size()]);
		}

		return EMPTY_HIGHLIGHTS;
	}

	protected Highlight[] getMnemonicDataTypeHighlights(String mnemonicText, Color highlightColor) {
		return new Highlight[] { new Highlight(0, mnemonicText.length() - 1, highlightColor) };
	}

	// returns null if there is no match
	private String getHighlightStringForParameterDeclaration(String parameterDeclaration) {
		String[] paramParts = parameterDeclaration.split("\\s");
		String paramName = paramParts[0];
		if (label.equals(paramName)) {
			return paramName;
		}
		// check for pointer names
		else if (label.endsWith("*") && label.startsWith(paramName)) {
			// see if we need to chop off some '*'s, as we may have searched for a pointer to a
			// pointer and have found a match against a simple pointer and thus the display may
			// not match our label
			if (paramParts.length == 1) {
				return paramName; // not a full declaration, just the name
			}

			String variableName = paramParts[paramParts.length - 1];
			int variableNameOffset = parameterDeclaration.lastIndexOf(variableName);
			if (label.length() > variableNameOffset) {
				return label.substring(0, variableNameOffset - 1); // -1 for the space before the name
			}

			return label;
		}

		return null;
	}

	private String getHighlightStringForDataTypeName(String listingDisplayText) {

		if (dataTypeName.equals(listingDisplayText)) {
			return listingDisplayText;
		}

		// check for pointer names
		if (dataTypeName.endsWith("*") && dataTypeName.startsWith(listingDisplayText)) {
			return listingDisplayText;
		}
		else if (listingDisplayText.startsWith(dataTypeName) && listingDisplayText.endsWith("*")) {
			return dataTypeName;
		}

		return null;
	}
}
