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

import java.util.ArrayList;
import java.util.List;

import ghidra.program.model.address.*;
import ghidra.program.model.data.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.mem.*;
import ghidra.program.model.symbol.*;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;

public class Vtable {

	private static final String VTABLE_LABEL = "vtable";

	Program program;
	Address vtableAddress;
	Boolean isSpecial = null;
	GccTypeinfoRef typeinfoRef = null;
	Address typeinfoRefAddress = null;
	GccTypeinfo typeinfo = null;
	Boolean hasVfunctions = null;
	Integer numVfunctions = null;
	Long topOffsetValue = null;
	Address vfunctionTop = null;
	Address typeinfoAddress = null;

	Boolean isPrimary = null;
	Boolean isConstruction = null;
	List<Vtable> internalVtables = new ArrayList<Vtable>();
	List<Address> relatedMainVtables = new ArrayList<Address>();
	boolean inExternalMemory;
	Boolean isValid = true;

	Namespace typeinfoNamespace = null;
	Namespace classNamespace = null;
	Integer length = null;
	Vtable primaryVtable = null;
	int defaultPointerSize;
	SymbolTable symbolTable;
	ExtendedFlatProgramAPI extendedFlatAPI;
	TaskMonitor monitor;
	GlobalNamespace globalNamespace;
	FunctionManager functionManager;
	DataTypeManager dataTypeManager;
	Listing listing;

	public Vtable(Program program, Address vtableAddress, GccTypeinfoRef typeinfoRef,
			boolean isSpecial, boolean inExternalMemory,
			Vtable primaryVtable, Boolean isConstruction, TaskMonitor monitor)
			throws CancelledException {

		this.program = program;
		this.vtableAddress = vtableAddress;
		this.typeinfoRef = typeinfoRef;
		this.isSpecial = isSpecial;
		this.inExternalMemory = inExternalMemory;
		this.primaryVtable = primaryVtable;
		this.isConstruction = isConstruction;
		this.monitor = monitor;
		this.typeinfoRefAddress = typeinfoRef.getAddress();
		this.typeinfo = (GccTypeinfo) typeinfoRef.getReferencedTypeinfo();
		if (this.typeinfo != null) {
			this.typeinfoNamespace = typeinfo.getNamespace();
		}

		AddressSpace addressSpace = vtableAddress.getAddressSpace();
		defaultPointerSize = addressSpace.getPointerSize();
		symbolTable = program.getSymbolTable();
		extendedFlatAPI = new ExtendedFlatProgramAPI(program, monitor);
		globalNamespace = (GlobalNamespace) program.getGlobalNamespace();
		functionManager = program.getFunctionManager();
		dataTypeManager = program.getDataTypeManager();
		listing = program.getListing();

		setup();
	}

	public Vtable(Program program, Address vtableAddress, GccTypeinfoRef typeinfoRef,
			boolean isSpecial, boolean inExternalMemory, TaskMonitor monitor)
			throws CancelledException {
		this(program, vtableAddress, typeinfoRef, isSpecial, inExternalMemory, null, null, monitor);
	}

	public Vtable(Program program, Address vtableAddress, GccTypeinfoRef typeinfoRef,
			boolean isSpecial, boolean inExternalMemory, boolean isConstruction,
			TaskMonitor monitor) throws CancelledException {
		this(program, vtableAddress, typeinfoRef, isSpecial, inExternalMemory, null, isConstruction,
			monitor);
	}

	protected void setup() throws CancelledException {

		checkValidTop();

		if (!isValid) {
			return;
		}

		setTopOffsetValue();

		if (!isValid) {
			return;
		}

		setIsInternalVtable();

		if (!isValid) {
			return;
		}

		figureOutNamespace();

		setHasVfunctions();

		if (!isValid) {
			return;
		}

		if (!isValid) {
			return;
		}

		setLength();

		if (!isValid) {
			return;
		}

		try {
			applyVtableData();
		}
		catch (Exception e) {
			isValid = false;
		}

		findInternalVtables();
	}

	public boolean equals(Vtable vtable) {
		return vtable.getAddress().equals(vtableAddress);
	}

	private void checkValidTop() {

		// check for existing vtable name - if has a non-default label that isn't vtable
		// then assume not valid
		Symbol symbol = symbolTable.getPrimarySymbol(vtableAddress);
		if (symbol != null && symbol.getSource() != SourceType.DEFAULT) {
			if (symbol.getName().contains(VTABLE_LABEL)) {
				isValid = true;
				return;
			}
			isValid = false;
			return;
		}

		// for ones with no symbol or default symbol check for long value/non address at
		// top
		// if is an address not valid
		// if is not an address may be valid - will need further checks in other methods
		Address referencedAddress = getReferencedAddress(vtableAddress);
		if (referencedAddress != null) {
			isValid = false;
			return;
		}
		// May or may not be valid but so far valid
		isValid = true;
	}

	public Address getAddress() {
		return vtableAddress;
	}

	public boolean isExternal() {
		return inExternalMemory;
	}

	public boolean isValid() {
		return isValid;
	}

	public Address getTypeinfoRefAddress() {
		return typeinfoRef.getAddress();
	}

	public GccTypeinfo getReferencedTypeinfo() {
		return (GccTypeinfo) typeinfoRef.getReferencedTypeinfo();
	}

	protected void setTypeinfoAddress() {

		typeinfoAddress = typeinfo.getAddress();
		typeinfoNamespace = typeinfo.getNamespace();

	}

	public Address getTypeinfoAddress() {
		return typeinfoAddress;
	}

	protected void setTopOffsetValue() {

		try {
			Address topOffset = typeinfoRefAddress.subtract(defaultPointerSize);
			if (topOffset.getOffset() < vtableAddress.getOffset()) {
				Msg.debug(this, "No offset field in vtable at " + vtableAddress.toString());
				isValid = false;
				return;
			}

			topOffsetValue = extendedFlatAPI.getLongValueAt(topOffset);
		}
		catch (IllegalArgumentException e) {
			Msg.debug(this, "Invalid vtable: " + vtableAddress.toString() + " No offset field");
			isValid = false;
		}
	}

	private void setIsInternalVtable() {

		if (topOffsetValue == null) {
			isValid = false;
			return;
		}

		// if it has an existing label that is exactly "vtable" then set it true and
		// return
		Symbol primarySymbol = symbolTable.getPrimarySymbol(vtableAddress);
		if (primarySymbol != null && primarySymbol.getName().equals("vtable")) {
			isPrimary = true;
			return;

		}

		// otherwise, use the topOffsetValue to figure it out
		if (topOffsetValue == 0L) {
			isPrimary = true;
		}
		else {
			isPrimary = false;
		}
	}

	public Boolean isPrimary() {

		if (isPrimary == null) {
			isValid = false;
			return null;
		}

		return isPrimary;
	}

	public void setNumVfunctions(int num) {
		numVfunctions = num;
	}

	public Integer getNumVfunctions() {

		if (numVfunctions == null) {
			isValid = false;
			return null;
		}
		return numVfunctions;
	}

	protected void setHasVfunctions() throws CancelledException {

		if (isPrimary == null) {
			isValid = false;
			return;
		}

		try {
			Address possVfunctionTop = typeinfoRefAddress.add(defaultPointerSize);

			numVfunctions = getNumFunctionPointers(possVfunctionTop, true, false);
			if (numVfunctions == 0) {
				hasVfunctions = false;
			}
			else {
				hasVfunctions = true;
				vfunctionTop = possVfunctionTop;
			}
		}
		catch (AddressOutOfBoundsException e) {
			hasVfunctions = false;
		}

	}

	private int getNumFunctionPointers(Address topAddress, boolean allowNullFunctionPtrs,
			boolean allowDefaultRefsInMiddle) throws CancelledException {

		int numFunctionPointers = 0;
		Address address = topAddress;

		// if it has a primary non-default symbol and it isn't "vftable" then it isn't a vftable
		Symbol primarySymbol = symbolTable.getPrimarySymbol(topAddress);
		if (primarySymbol != null && primarySymbol.getSource() != SourceType.DEFAULT &&
			!primarySymbol.getName().contains("vftable")) {
			return numFunctionPointers;
		}
		MemoryBlock currentBlock = program.getMemory().getBlock(topAddress);

		boolean stillInCurrentTable = true;
		while (address != null && currentBlock.contains(address) && stillInCurrentTable &&
			(extendedFlatAPI.isPossibleFunctionPointer(program, address) ||
				(allowNullFunctionPtrs && isPossibleNullPointer(address)))) {

			numFunctionPointers++;
			address = address.add(defaultPointerSize);
			Symbol symbol = symbolTable.getPrimarySymbol(address);
			if (symbol == null) {
				continue;
			}
			// never let non-default refs in middle
			if (symbol.getSource() != SourceType.DEFAULT) {
				stillInCurrentTable = false;
			}

			// if it gets here it is default
			if (!allowDefaultRefsInMiddle) {
				stillInCurrentTable = false;
			}
		}

		//NEW: TESTING Don't allow single null pointer at top of vftable
		//OR test to see if nulls then typeinfo ptr
//		if(isPossibleNullPointer(topAddress) && numFunctionPointers == 1) {
//				return 0;
//		}

		// check to see if last is null ptr and next addr after that is typeinfo ref - indicating the null is really top of next vtable
		Address lastAddress = topAddress.add((numFunctionPointers - 1) * defaultPointerSize);
		if (isPossibleNullPointer(lastAddress) &&
			(isTypeinfoRef(lastAddress.add(defaultPointerSize)))) {
			numFunctionPointers--;
		}
		return numFunctionPointers;
	}

	private boolean isTypeinfoRef(Address addr) {

		Address referencedAddress = getReferencedAddress(addr);
		if (referencedAddress == null) {
			return false;
		}
		Data data = program.getListing().getDataAt(referencedAddress);
		if (data == null) {
			return false;
		}

		if (data.getBaseDataType().getName().contains("ClassTypeInfoStructure")) {
			return true;
		}
		return false;
	}

	/**
	 * Method to determine if there are enough zeros to make a null poihnter and no
	 * references into or out of the middle
	 * 
	 * @param address the given address
	 * @return true if the given address could be a valid null pointer, false if not
	 */
	private boolean isPossibleNullPointer(Address address) throws CancelledException {
		if (!extendedFlatAPI.hasNumZeros(address, defaultPointerSize)) {
			return false;
		}
		return true;
	}

	public void setHasVfunctions(boolean flag) {
		hasVfunctions = flag;
	}

	public boolean hasVfunctions() {
		return hasVfunctions;
	}

	public Address getVfunctionTop() {
		return vfunctionTop;
	}

	protected void setLength() {

		if (hasVfunctions == null) {
			isValid = false;
			return;
		}
		if (!hasVfunctions) {
			length = (int) (typeinfoRefAddress.getOffset() + defaultPointerSize -
				vtableAddress.getOffset());
			return;
		}

		if (numVfunctions == null || vfunctionTop == null) {
			isValid = false;
			return;
		}

		Address endAddr = vfunctionTop.add(numVfunctions * defaultPointerSize);
		length = (int) (endAddr.getOffset() - vtableAddress.getOffset());

	}

	private void addToLength(int amountToAdd) {
		length = length + amountToAdd;
	}

	public int getLength() {
		return length;
	}

	public List<Long> getOffsets() {

		List<Long> offsetValues = new ArrayList<Long>();
		offsetValues.add(topOffsetValue);

		// skip to next offset above the first one which we already knew and have added 
		// add all the values until all including the value at the vtableAddress are added
		Address nextOffsetAddress = typeinfoRefAddress.subtract(2 * defaultPointerSize);
		while (nextOffsetAddress.getOffset() <= vtableAddress.getOffset()) {
			offsetValues.add(extendedFlatAPI.getLongValueAt(nextOffsetAddress));
			nextOffsetAddress = nextOffsetAddress.subtract(defaultPointerSize);
		}

		return offsetValues;
	}

	private void findInternalVtables() throws CancelledException {

		// if the current table is already an internal vtable there won't be any
		// internal ones in it
		if (!isPrimary) {
			return;
		}

		boolean keepChecking = true;

		int limit = length;

		while (keepChecking) {

			monitor.checkCancelled();

			Address nextAddr = vtableAddress.add(length);

			Address typeinfoAddr = typeinfo.getAddress();

			int alignment = nextAddr.getSize() / 8;
			Address nextTypeinfoRefAddr =
				getNextReferenceTo(nextAddr, typeinfoAddr, alignment, limit);
			if (nextTypeinfoRefAddr == null) {
				keepChecking = false;
				continue;
			}

			GccTypeinfoRef internalTypenfoRef =
				new GccTypeinfoRef(nextTypeinfoRefAddr, typeinfo, true);

			Vtable possibleInternalVtable =
				new Vtable(program, nextAddr, internalTypenfoRef, isSpecial, inExternalMemory,
					this, isConstruction, monitor);
			if (!possibleInternalVtable.isValid()) {
				keepChecking = false;
				continue;
			}

			if (possibleInternalVtable.isPrimary()) {
				keepChecking = false;
				continue;
			}

			Namespace internalVtableNamespace = possibleInternalVtable.getNamespace();
			if (internalVtableNamespace != null && internalVtableNamespace.equals(classNamespace)) {
				addInternalVtable(possibleInternalVtable);
			}
			else {
				keepChecking = false;
			}

		}
	}

	private Address getNextReferenceTo(Address startAddress, Address refdAddress, int alignment,
			int limit) {

		int offset = alignment;
		while (offset < limit) {
			Address addr = startAddress.add(offset);
			Address referencedAddress = getReferencedAddress(addr);
			if (referencedAddress != null && referencedAddress.equals(refdAddress)) {
				return addr;
			}
			offset += alignment;
		}
		return null;
	}

	private void addInternalVtable(Vtable internalVtable) {
		internalVtables.add(internalVtable);
		addToLength(internalVtable.getLength());
	}

	public List<Vtable> getInternalVtables() {
		return internalVtables;
	}

	public void setIsConstructionVtable(Boolean setting) {
		isConstruction = setting;
	}

	public Boolean isConstructionVtable() {
		return isConstruction;
	}

	private void figureOutNamespace() {

		if (isConstruction == null) {
			setNamespace(globalNamespace);
			return;
		}

		// if construction vtable can't figure out from within vtable object
		// have to assign later after further inspection of vtts and other info
		if (isConstruction != null && isConstructionVtable()) {
			setNamespace(globalNamespace);
			return;
		}

		if (isPrimary()) {
			setNamespace(typeinfoNamespace);
			return;
		}

		// if not primary and the primary has same namespace then it is an internal
		// vtable and can
		// set the namespace to the typeinfo namespace
		if (!primaryVtable.getNamespace().isGlobal() &&
			primaryVtable.getNamespace().equals(typeinfoNamespace)) {
			setNamespace(typeinfoNamespace);
			return;
		}

	}

	// for setting namespaces after vtable is created when they can't
	// be determined by looking at internals of current vtable
	public void setNamespace(Namespace namespace) {

		classNamespace = namespace;
		for (Vtable internalVtable : internalVtables) {
			internalVtable.setNamespace(namespace);
		}
	}

	public Namespace getNamespace() {
		return classNamespace;
	}

	protected boolean applyVtableData() throws CancelledException, Exception {

		Data dataAt = listing.getDataAt(vtableAddress);

		// first check to see it is an erroneous vtable that has been made a byte array
		// if so, clear it and start looking for the typeinfo reference
		//TODO: check !isDefined and use known length to clear
		if (dataAt != null && dataAt.isArray()) {
			listing.clearCodeUnits(vtableAddress, vtableAddress, false);
		}

		if (dataAt != null && !dataAt.getDataType().getName().equals("long")) {
			listing.clearCodeUnits(vtableAddress, vtableAddress, false);
		}

		// create the typeinfo pointer if there isn't already one
		Data typeinfoPtr = listing.getDataAt(typeinfoRefAddress);
		if (typeinfoPtr == null || !typeinfoPtr.isDefined()) {
			DataType nullPointer = dataTypeManager.getPointer(null);

			listing.createData(typeinfoRefAddress, nullPointer);

		}

		// create longs from top of vtable to the typeinfo reference
		createLongs(this.vtableAddress, typeinfoRefAddress);

		int numFunctionPointers = getNumVfunctions();

		if (numFunctionPointers != 0) {

			Address vftableAddress = getVfunctionTop();

			createVftableArray(vftableAddress, numFunctionPointers);
		}

		return true;
	}

	public Data createVftableArray(Address vftableAddress, int numFunctionPointers)
			throws AddressOutOfBoundsException {

		listing.clearCodeUnits(vftableAddress,
			vftableAddress.add((numFunctionPointers * defaultPointerSize - 1)), false);

		DataType pointerDataType = dataTypeManager.getPointer(null);
		ArrayDataType vftableArrayDataType = new ArrayDataType(pointerDataType, numFunctionPointers,
			defaultPointerSize);
		try {
			Data vftableArrayData = listing.createData(vftableAddress, vftableArrayDataType);
			return vftableArrayData;
		}
		catch (Exception e) {
			return null;
		}

	}

	/**
	 * Method to create a series of long data types from the given start address to
	 * the given end address
	 * 
	 * @param start the starting address
	 * @param end   the ending address
	 * @throws CancelledException if cancelled
	 * @throws Exception          if data has conflict when created
	 */
	private void createLongs(Address start, Address end) throws CancelledException, Exception {

		DataType longDT = new LongDataType(dataTypeManager);
		if (defaultPointerSize == 8) {
			longDT = new LongLongDataType();
		}
		int offset = 0;
		Address address = start;
		while (address != null && !address.equals(end)) {

			listing.clearCodeUnits(address, address.add(defaultPointerSize - 1), false);
			listing.createData(address, longDT);
			offset += defaultPointerSize;
			address = getAddress(start, offset);
		}

	}

	/**
	 * Method to get address at address + offset
	 * 
	 * @param address the given address
	 * @param offset  the given offset
	 * @return the address at address + offset or null if it doesn't exist
	 */
	private Address getAddress(Address address, int offset) {
		try {
			Address newAddress = address.add(offset);
			return newAddress;
		}
		catch (AddressOutOfBoundsException e) {
			return null;
		}
	}

	private Address getReferencedAddress(Address address) {

		int addressSize = address.getSize();
		Memory memory = program.getMemory();
		try {

			if (addressSize == 32) {
				long offset32 = memory.getInt(address);
				Address newAddr = address.getNewAddress(offset32);
				if (memory.contains(newAddr)) {
					return newAddr;
				}
				return null;

			}
			else if (addressSize == 64) {

				long offset64 = memory.getLong(address);
				Address newAddr = address.getNewAddress(offset64);
				if (memory.contains(newAddr)) {
					return newAddr;
				}
				return null;

			}
			else {
				return null;
			}
		}
		catch (MemoryAccessException e) {
			return null;
		}
	}

}
