/* ###
 * 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.util.bin.format.dwarf;

import ghidra.program.model.address.Address;
import ghidra.program.model.data.*;
import ghidra.program.model.data.Enum;
import ghidra.program.model.listing.*;

/**
 * Logic to test if a Data instance is replaceable with a data type. 
 */
public class DWARFDataInstanceHelper {
	private Program program;
	private Listing listing;
	private boolean allowTruncating = true;

	public DWARFDataInstanceHelper(Program program) {
		this.program = program;
		this.listing = program.getListing();
	}

	public DWARFDataInstanceHelper setAllowTruncating(boolean b) {
		this.allowTruncating = b;
		return this;
	}

	private boolean isArrayDataTypeCompatibleWithExistingData(Array arrayDT, Data existingData) {

		DataType existingDataDT = existingData.getBaseDataType();
		if (existingDataDT.isEquivalent(arrayDT)) {
			return true;
		}

		DataType elementDT = arrayDT.getDataType();
		if (elementDT instanceof TypeDef typedef) {
			elementDT = typedef.getBaseDataType();
		}
		
		DataType existingElementDT = existingDataDT instanceof Array existingArrayDT 
				? existingArrayDT.getDataType() 
				: null;
		if (elementDT instanceof CharDataType && existingDataDT instanceof StringDataType) {
			// hack to allow a char array to overwrite a string in memory
			existingElementDT = elementDT;
		}
		if (existingElementDT instanceof TypeDef typedef) {
			existingElementDT = typedef.getBaseDataType();
		}
		
		if (existingDataDT instanceof Array || existingDataDT instanceof StringDataType) {
			if (!existingElementDT.isEquivalent(elementDT)) {
				return false;
			}

			if (arrayDT.getLength() == existingData.getLength()) {
				return true;
			}
			if (arrayDT.getLength() < existingData.getLength()) {
				// if proposed array is smaller than in-memory array
				return allowTruncating;
			}

			// if proposed array is longer than in-memory array, check if there is only 
			// undefined data following the in-memory array
			return hasTrailingUndefined(existingData, arrayDT);
		}

		// existing data wasn't an array, test each location the proposed array would overwrite
		Address address = existingData.getAddress();
		for (int i = 0; i < arrayDT.getNumElements(); i++) {
			Address elementAddress = address.add(arrayDT.getElementLength() * i);
			Data data = listing.getDataAt(elementAddress);
			if (data != null && !isDataTypeCompatibleWithExistingData(elementDT, data)) {
				return false;
			}
		}

		return true;
	}

	private boolean hasTrailingUndefined(Data data, DataType replacementDT) {
		Address address = data.getAddress();
		return DataUtilities.isUndefinedRange(program, address.add(data.getLength()),
			address.add(replacementDT.getLength() - 1));
	}

	private boolean isStructDataTypeCompatibleWithExistingData(Structure structDT,
			Data existingData) {
		DataType existingDataDT = existingData.getBaseDataType();
		if (existingDataDT instanceof Structure) {
			return existingDataDT.isEquivalent(structDT);
		}

		// existing data wasn't a structure, test each location the proposed structure would overwrite
		Address address = existingData.getAddress();
		for (DataTypeComponent dtc : structDT.getDefinedComponents()) {
			Address memberAddress = address.add(dtc.getOffset());
			Data data = listing.getDataAt(memberAddress);
			if (data != null && !isDataTypeCompatibleWithExistingData(dtc.getDataType(), data)) {
				return false;
			}
		}
		boolean isTruncating = structDT.getLength() < existingData.getLength();
		return !isTruncating || allowTruncating;
	}

	private boolean isPointerDataTypeCompatibleWithExistingData(Pointer pdt, Data existingData) {
		DataType existingDT = existingData.getBaseDataType();

		// allow 'upgrading' an integer type to a pointer
		boolean isRightType =
			(existingDT instanceof Pointer) || (existingDT instanceof AbstractIntegerDataType);
		return isRightType && existingDT.getLength() == pdt.getLength();
	}

	private boolean isSimpleDataTypeCompatibleWithExistingData(DataType simpleDT,
			Data existingData) {
		// simpleDT will be int, char, float, or string data types
		
		boolean isSameLen = isSameLen(simpleDT, existingData);
		DataType existingDT = existingData.getBaseDataType();

		if (simpleDT instanceof CharDataType && existingDT instanceof StringDataType) {
			// char overwriting a string
			return isSameLen || allowTruncating;
		}

		if (Undefined.isUndefined(existingDT) && isSameLen) {
			// some type overwriting an undefined
			return true;
		}

		return simpleDT.getClass().isInstance(existingDT) && isSameLen;
	}

	private boolean isSameLen(DataType dt, Data existingData) {
		return existingData.getLength() == dt.getLength() ||
			(dt instanceof Dynamic dyDT && dyDT.canSpecifyLength());
	}

	private boolean isEnumDataTypeCompatibleWithExistingData(Enum enumDT, Data existingData) {
		// This is a very fuzzy check to see if the value located at address is compatible.
		// Match if its an enum or integer with correct size.  The details about enum
		// members are ignored.
		DataType existingDT = existingData.getBaseDataType();
		if (!(existingDT instanceof Enum || existingDT instanceof AbstractIntegerDataType)) {
			return false;
		}
		if (existingDT instanceof BooleanDataType) {
			return false;
		}
		if (existingDT.getLength() != enumDT.getLength()) {
			return false;
		}
		return true;
	}

	private boolean isDataTypeCompatibleWithExistingData(DataType dataType, Data existingData) {
		if (existingData == null || !existingData.isDefined()) {
			return true;
		}

		if (dataType instanceof Array) {
			return isArrayDataTypeCompatibleWithExistingData((Array) dataType, existingData);
		}
		if (dataType instanceof Pointer) {
			return isPointerDataTypeCompatibleWithExistingData((Pointer) dataType, existingData);
		}
		if (dataType instanceof Structure) {
			return isStructDataTypeCompatibleWithExistingData((Structure) dataType, existingData);
		}
		if (dataType instanceof TypeDef) {
			return isDataTypeCompatibleWithExistingData(((TypeDef) dataType).getBaseDataType(),
				existingData);
		}
		if (dataType instanceof Enum) {
			return isEnumDataTypeCompatibleWithExistingData((Enum) dataType, existingData);
		}

		if (dataType instanceof AbstractIntegerDataType ||
			dataType instanceof AbstractFloatDataType || dataType instanceof StringDataType ||
			dataType instanceof WideCharDataType || dataType instanceof WideChar16DataType ||
			dataType instanceof WideChar32DataType) {
			return isSimpleDataTypeCompatibleWithExistingData(dataType, existingData);
		}

		return false;
	}

	public boolean isDataTypeCompatibleWithAddress(DataType dataType, Address address) {
		if (DataUtilities.isUndefinedRange(program, address,
			address.add(dataType.getLength() - 1))) {
			return true;
		}

		Data data = listing.getDataContaining(address);
		if (data == null) {
			return false; 	// will only get null if something is really screwed up
		}
		if (!data.getMinAddress().equals(address)) {
			return false;  // was pointing to something in the middle of an existing data instance
		}
		return isDataTypeCompatibleWithExistingData(dataType, data);
	}

}
