/* CreateSkeleton
 * Creates a skeleton PlayStation dev project based on the output of SymDumpTE
 * or another source of the appropriate JSON data
 * Copyright 2019 Ben Lincoln
 * https://www.beneaththewaves.net/
 * 
 * This file is part of CreateSkeleton.
 * 
 * CreateSkeleton is free software: you can redistribute it and/or modify
 * it under the terms of version 3 of the GNU General Public License as published by
 * the Free Software Foundation.
 * 
 * CreateSkeleton is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with CreateSkeleton (in the file LICENSE.txt).  
 * If not, see <http://www.gnu.org/licenses/>.
 */
// %PROJECT_NAME%: Machine-generated data-exporting script

import java.io.*;
import java.util.*;
import java.nio.charset.StandardCharsets;

import ghidra.app.plugin.core.script.Ingredient;
import ghidra.app.plugin.core.script.IngredientDescription;
import ghidra.app.script.GatherParamPanel;
import ghidra.app.script.GhidraScript;
import ghidra.program.flatapi.FlatProgramAPI;
import ghidra.program.model.address.*;
import ghidra.program.model.block.*;
import ghidra.program.model.data.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.mem.*;
import ghidra.program.model.lang.*;
import ghidra.program.model.symbol.*;

import utilities.util.FileUtilities;

public class %PROJECT_NAME%TDRExportData extends GhidraScript implements Ingredient {
	
	// This class is a workaround for not being able to reference external code
	// without compiling it, which would mean recompiling every time there's a new 
	// Ghidra release.
	// Tt would be static, except that Java doesn't allow classes defined within 
	// other classes to be static.
	// Without doing it this way, utility methods used by more than one class in 
	// this script would need to go in both classes.
	public class LocalUtility
	{
		public List<String> CharTypeNames;
		public List<String> StringTypeNames;
		
		public int MaximumNonPrintableCharactersInCharArray = 1;
	
		public LocalUtility()
		{
			String[] charTypeNameArray = new String[] { "char", "uchar", "unsigned char" };
			CharTypeNames = Arrays.asList(charTypeNameArray);
			String[] stringTypeNameArray = new String[] { "string" };
			StringTypeNames = Arrays.asList(stringTypeNameArray);
		}
		
		public void PrintDebugMessage(String msg)
		{
			//println("Debug: " + msg);
		}
		
		public void PrintInfoMessage(String msg)
		{
			println("Info: " + msg);
		}
		
		public void PrintWarningMessage(String msg)
		{
			println("Warning: " + msg);
		}

		public void PrintErrorMessage(String msg)
		{
			println("Error: " + msg);
		}
		
		// Ghidra represents C chars using the Java Character class instead of bytes, 
		// This basically promots them to the 0x00-0xFF range of Unicode.
		// This method is a workaround to basically get a byte back.
		// They're handled as ints to avoid a situation where unexpectedly the data is 
		// actually Unicode
		protected int GetCharNumFromData(Data d)
		{
			if (d.getValue() == null)
			{
				return 0;
			}
			if (d.getValue().equals("null"))
			{
				return 0;
			}
			if (d.getValue().equals("\0"))
			{
				return 0;
			}
			if (d.getValue().equals("\\0"))
			{
				return 0;
			}
			Character valueChar = (Character)d.getValue();		
			int result = (int)valueChar;
			PrintDebugMessage("calculated char num " + String.valueOf(result) + " for input char '" + valueChar + "'.");
			if (result > 255)
			{
				println("Error: got a value greater than 0xFF for char at " + LongAsHexString(d.getAddress().getOffset()) + ": " + String.format("0x%0X", result));
			}
			return result;
		}
		
		protected int[] GetGhidraCharArrayAsCharNumAray(List<Data> charArray)
		{
			//int[] result = new int[charArray.size()];
			List<Integer> resultList = new ArrayList<Integer>();
			for (int i = 0; i < charArray.size(); i++)
			{
				Data d = charArray.get(i);
				boolean handleAsChar = true;
				try
				{
					Character valueChar = (Character)d.getValue();
				}
				catch (java.lang.ClassCastException cce)
				{
					handleAsChar = false;
				}
				if (handleAsChar)
				{
					resultList.add(GetCharNumFromData(d));
				}
				else
				{
					String valueString = String.valueOf(d.getValue());
					byte[] stringBytes = valueString.getBytes(StandardCharsets.US_ASCII);
					for (int byteNum = 0; byteNum < stringBytes.length; byteNum++)
					{
						resultList.add(Integer.valueOf(stringBytes[byteNum]));
					}
				}
			}
			int[] result = new int[resultList.size()];
			for (int i = 0; i < resultList.size(); i++)
			{
				result[i] = resultList.get(i);
			}
			return result;
		}
		
		protected int GetCountOfNonPrintableCharactersInCharArray(List<Data> charArray, boolean nullIsNonPrintable)
		{
			int result = 0;
			int[] arrayChars = GetGhidraCharArrayAsCharNumAray(charArray);
			
			for (int i = 0; i < arrayChars.length; i++)
			{
				boolean ignore = false;
				// ignore non-printable formatting characters
				// newline
				if (arrayChars[i] == 0x0A)
				{
					ignore = true;
				}
				// carriage return
				if (arrayChars[i] == 0x0D)
				{
					ignore = true;
				}
				// tab
				if (arrayChars[i] == 0x09)
				{
					ignore = true;
				}
				if ((!nullIsNonPrintable) && (arrayChars[i] == 0x00))
				{
					ignore = true;
				}
				
				if (!ignore)
				{
					if ((arrayChars[i] < 0x20) || ((arrayChars[i] > 0x7E) && (arrayChars[i] <= 0xFF)))
					{
						result++;
					}
				}
			}
			return result;
		}

		protected int GetIndexOfNullByteInCharArray(List<Data> charArray, int startOffset)
		{
			int result = 0;
			int[] arrayChars = GetGhidraCharArrayAsCharNumAray(charArray);
			
			for (int i = startOffset; i < arrayChars.length; i++)
			{
				if (arrayChars[i] == 0x00)
				{
					return i;
				}
			}
			return -1;
		}
		
		protected int GetIndexOfFirstNullByteInCharArray(List<Data> charArray)
		{
			return GetIndexOfNullByteInCharArray(charArray, 0);
		}
	
		public String GetSymbolNameWithoutBadChars(String unescapedString)
		{
			StringBuffer result = new StringBuffer();
			byte[] stringBytes = unescapedString.getBytes(StandardCharsets.US_ASCII);
			Byte[] badChars = new Byte[]
			{
				0x21,	// !
				0x22,	// "
				0x23,	// #
				0x24,	// $
				0x25,	// %
				0x26,	// &
				0x27,	// '
				0x28,	// (
				0x29,	// )
				0x2A,	// *
				0x2B,	// +
				0x2C,	// ,
				//0x2E,	// .
				0x2F,	// /
				0x3A,	// :
				0x3B,	// ;
				0x3C,	// <
				0x3D,	// =
				0x3E,	// >
				0x3F,	// ?
				0x40,	// @
				0x5B,	// [
				0x5C,	// backslash
				0x5D,	// ]
				0x5E,	// ^
				0x60,	// `
			};
			List<Byte> badByteList = Arrays.asList(badChars);
			
			for (int i = 0; i < stringBytes.length; i++)
			{
				boolean handled = false;
				if ((stringBytes[i] < 0x21) || (stringBytes[i] > 0x7A))
				{
					handled = true;
				}
				if ((!handled) && (badByteList.contains(stringBytes[i])))
				{
					handled = true;
				}
				if ((!handled) && (stringBytes[i] == 0x2E))
				{
					result.append("_");
					handled = true;
				}
			
				if (!handled)
				{
					result.append(Character.toString(stringBytes[i]));
				}
			}
			
			return result.toString();
		}
		
		public String GetCValueForAssignment(DataType dt, String cValue)
		{
			StringBuffer buffer = new StringBuffer();
			boolean handled = false;
			boolean isString = false;
			if (dt.getName() == "string")
			{
				isString = true;
			}
			if (isString)
			{
				//buffer.append("\"");
				buffer.append(cValue);
				//buffer.append("\"");
				handled = true;
			}
			if (!handled)
			{
				buffer.append(cValue);
			}
			return buffer.toString();
		}
		
		public String GetShortStats(DataDescription dd)
		{
			StringBuffer buffer = new StringBuffer();
			buffer.append(dd.BaseDataType.getName());
			buffer.append(" @");
			buffer.append(LongAsHexString(dd.Address.getOffset()));
			buffer.append(", len = ");
			buffer.append(LongAsHexString(dd.Length));
			return buffer.toString();
		}
		
		public String GetShortStatsComment(DataDescription dd)
		{
			StringBuffer buffer = new StringBuffer();
			buffer.append("// ");
			buffer.append(GetShortStats(dd));
			return buffer.toString();
		}
		
		protected String LongAsHexString(long l)
		{
			return String.format("0x%08X", l);
		}
		
		protected String IntAsHexString(int i)
		{
			return String.format("0x%04X", i);
		}
		
		protected String ByteAsHexString(byte b)
		{
			return String.format("0x%02X", b);
		}
		
		protected String GetEscapedChar(byte inputCharNum)
		{
			char inputChar = (char)inputCharNum;
			if (inputChar == '\n')
			{
				return "\\n";
			}
			if (inputChar == '\r')
			{
				return "\\r";
			}
			if (inputChar == '\t')
			{
				return "\\t";
			}
			if (inputChar == '"')
			{
				return "\\\"";
			}
			if ((inputCharNum < 32) || (inputCharNum > 126))
			{
				return String.format("\\x%02X", inputCharNum);
			}
				
			return Character.toString(inputChar);
		}
		
		protected String GetEscapedString(String unescapedString)
		{
			StringBuffer result = new StringBuffer();
			byte[] stringBytes = unescapedString.getBytes(StandardCharsets.US_ASCII);
			for (int i = 0; i < stringBytes.length; i++)
			{
				result.append(GetEscapedChar(stringBytes[i]));
			}
			
			return result.toString();
		}
		
		protected String ArrayToCSV(String[] entries)
		{
			StringBuffer b = new StringBuffer();
			b.append("[");
			for (int i = 0; i < entries.length; i++)
			{
				b.append(entries[i]);
				if (i < (entries.length - 1))
				{
					b.append(", ");
				}
			}
			b.append("]");
			return b.toString();
		}
		
		protected String[] SymbolArrayToStringArray(Symbol[] symbols)
		{
			String[] list = new String[symbols.length];
			for (int i = 0; i < symbols.length; i++)
			{
				list[i] = symbols[i].getName();
			}
			return list;
		}
		
		protected String[] ReferenceFromArrayToStringArray(Reference[] refs)
		{
			String[] list = new String[refs.length];
			for (int i = 0; i < refs.length; i++)
			{
				list[i] = LongAsHexString(refs[i].getFromAddress().getOffset());
				String refName = GetLabelOrDynamicName(refs[i].getFromAddress());
				if ((refName != null) && (!refName.equals("")))
				{
					list[i] = list[i] + " (" + refName + ")";
				}
			}
			return list;
		}
		
		protected String GetLabelOrDynamicName(Address addr)
		{
			SymbolTable symbolTable = currentProgram.getSymbolTable();
			LocalUtility u = new LocalUtility();
			String result = "";
			try
			{
				Symbol stResult = symbolTable.getPrimarySymbol(addr);
				if (stResult != null)
				{
					result = stResult.getName();
					if (!result.equals(""))
					{
						return result;
					}
				}
			}
			catch (Exception e)
			{
				result = "[" + e.toString() + " thrown while getting the primary symbol for the item at " + u.LongAsHexString(addr.getOffset()) + "]";
			}
			try
			{
				result = SymbolUtilities.getDynamicName​(currentProgram, addr);
				if ((result != null) && (!result.equals("")))
				{
					return result;
				}
			}
			catch (Exception e)
			{
				result = "[" + e.toString() + " thrown while getting the dynamic name for the item at " + u.LongAsHexString(addr.getOffset()) + "]";
			}
			return "[unable to obtain either a symbol or a dynamic name for the item at " + u.LongAsHexString(addr.getOffset()) + "]";
		}
	}
	
	public class DataDescription
	{
		public Data BaseData;
		public DataType BaseDataType;
		public Address Address;
		public long Length;
		public String Name;
		public String FieldName;
		public Symbol[] Symbols;
		public Symbol PrimarySymbol;
		public String DynamicName;
		public String DataTypeName;
		public DataDescription ParentDataDescription;
		public List<DataDescription> ChildDataDescriptions;
		public Reference[] ReferencesTo;
		public Reference[] ReferencesFrom;
		public String Value;

		public DataDescription()
		{
			ChildDataDescriptions = new ArrayList<DataDescription>();
		}
		
		public String GetTabDelimitedDescription(int recursionDepth)
		{
			StringBuffer buffer = new StringBuffer();
			LocalUtility u = new LocalUtility();

			if (recursionDepth > 0)
			{
				for (int i = 0; i < recursionDepth; i++)
				{
					buffer.append("\t");
				}
			}
			
			buffer.append(u.LongAsHexString(Address.getOffset()));
			buffer.append("\t");
			buffer.append(u.LongAsHexString(Length));
			buffer.append("\t");
			buffer.append(u.ArrayToCSV(u.SymbolArrayToStringArray(Symbols)));
			buffer.append("\t");
			if (PrimarySymbol != null)
			{
				buffer.append(PrimarySymbol.getName());
			}
			buffer.append("\t");
			buffer.append(DynamicName);
			buffer.append("\t");
			buffer.append(BaseData.getFieldName());
			buffer.append("\t");
			buffer.append(BaseDataType.getName());
			buffer.append("\t");
			buffer.append(BaseDataType.getDisplayName());
			buffer.append("\t");
			buffer.append(BaseData.getMnemonicString());
			buffer.append("\t");
			buffer.append(u.ArrayToCSV(u.ReferenceFromArrayToStringArray(ReferencesTo)));
			buffer.append("\t");
			buffer.append(u.ArrayToCSV(u.ReferenceFromArrayToStringArray(ReferencesFrom)));
			buffer.append("\t");
			buffer.append(Value);
			int subComponentCount = ChildDataDescriptions.size();
			if (subComponentCount > 0)
			{
				for (int scNum = 0; scNum < subComponentCount; scNum++)
				{
					buffer.append(System.lineSeparator());
					buffer.append(ChildDataDescriptions.get(scNum).GetTabDelimitedDescription(recursionDepth + 1));
				}
			}

			return buffer.toString();

		}
		
		public String GetBestName()
		{
			if (PrimarySymbol != null)
			{
				return PrimarySymbol.getName();
			}
			return DynamicName;
		}

		public String GetCCode(int recursionDepth, boolean parentIsArray)
		{
			StringBuffer buffer = new StringBuffer();
			LocalUtility u = new LocalUtility();

			int subComponentCount = ChildDataDescriptions.size();
			boolean noExplicitName = false;
			
			String tabs = "";
			int nextRecursionDepth = recursionDepth + 1;
			/*
			if (nextRecursionDepth == 1)
			{
				nextRecursionDepth = 2;
			}
			*/
			if (recursionDepth > 0)
			{
				for (int i = 0; i < recursionDepth; i++)
				{
					tabs = tabs + "\t";
				}				
			}
			
			if (recursionDepth == 0)
			{
				buffer.append("// TDR: BEGIN GLOBAL VARIABLE");
				buffer.append(System.lineSeparator());
				String itemName = "";
				if (PrimarySymbol != null)
				{
					buffer.append("// TDR: Primary Name: ");
					itemName = PrimarySymbol.getName();
					buffer.append(itemName);
					buffer.append(System.lineSeparator());
				}
				buffer.append("// TDR: Dynamic Name: ");
				buffer.append(DynamicName);
				if (itemName.equals(""))
				{
					itemName = DynamicName;
					noExplicitName = true;
				}
				buffer.append(System.lineSeparator());
				itemName = u.GetSymbolNameWithoutBadChars(itemName);
				buffer.append("// TDR: Name in Code: ");
				buffer.append(itemName);
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: Address: ");
				buffer.append(u.LongAsHexString(Address.getOffset()));
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: Data Type Name: ");
				buffer.append(BaseDataType.getName());
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: Data Type Display Name: ");
				buffer.append(BaseDataType.getDisplayName());
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: Length: ");
				buffer.append(u.LongAsHexString(Length));
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: Mnemonic String: ");
				buffer.append(BaseData.getMnemonicString());
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: Symbols: ");
				buffer.append(u.ArrayToCSV(u.SymbolArrayToStringArray(Symbols)));
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: References To: ");
				buffer.append(u.ArrayToCSV(u.ReferenceFromArrayToStringArray(ReferencesTo)));
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: References From: ");
				buffer.append(u.ArrayToCSV(u.ReferenceFromArrayToStringArray(ReferencesFrom)));
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: BEGIN C HEADER CONTENT");
				buffer.append(System.lineSeparator());
				buffer.append(tabs);
				buffer.append(u.GetShortStatsComment(this));
				buffer.append(System.lineSeparator());
				buffer.append("extern ");
				if (BaseData.isVolatile())
				{
					buffer.append("volatile ");
				}
				if (BaseData.isConstant())
				{
					buffer.append("const ");
				}
				if (BaseData.isStructure())
				{
					buffer.append("struct ");
				}
				if (BaseData.isUnion())
				{
					buffer.append("union ");
				}
				buffer.append(BaseDataType.getName());
				buffer.append(" ");
				buffer.append(itemName);
				buffer.append(";");
				//buffer.append(u.GetShortStatsComment(this));
				if (noExplicitName)
				{
					buffer.append(" // WARNING: no name explicitly defined");
				}
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: END C HEADER CONTENT");
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: BEGIN C CODE");
				buffer.append(System.lineSeparator());
				buffer.append(tabs);
				buffer.append(u.GetShortStatsComment(this));
				buffer.append(System.lineSeparator());
				buffer.append(itemName);
				buffer.append(" = ");
				if (subComponentCount > 0)
				{
					buffer.append(System.lineSeparator());
					buffer.append("{");
					buffer.append(System.lineSeparator());
				}
				else
				{
					buffer.append(u.GetCValueForAssignment(BaseDataType, Value));
					buffer.append(";");
					if (noExplicitName)
					{
						buffer.append(" // WARNING: no name explicitly defined");
					}
				}
				
				//buffer.append(System.lineSeparator());
			}

			if (subComponentCount > 0)
			{
				for (int scNum = 0; scNum < subComponentCount; scNum++)
				{
					DataDescription subData = ChildDataDescriptions.get(scNum);
					boolean includeShortStats = true;
					// do not use named fields for array members, only structs/unions
					boolean isArrayMember = false;
					String fieldName = subData.BaseData.getFieldName();
					if ((fieldName != null) && (fieldName.substring(0, 1).equals("[")))
					{
						isArrayMember = true;
					}
					if (isArrayMember)
					{
						if (subData.BaseDataType.getName().equals("char"))
						{
							includeShortStats = false;
						}
						
						if (subData.BaseDataType.getName().equals("short"))
						{
							includeShortStats = false;
						}
						if (subData.BaseDataType.getName().equals("int"))
						{
							includeShortStats = false;
						}
						if (subData.BaseDataType.getName().equals("uint"))
						{
							includeShortStats = false;
						}
						if (subData.BaseDataType.getName().equals("unsigned int"))
						{
							includeShortStats = false;
						}
						if (subData.BaseDataType.getName().equals("long"))
						{
							includeShortStats = false;
						}
						
					}
					if (includeShortStats)
					{
						buffer.append(tabs);
						buffer.append("\t");
						buffer.append(u.GetShortStatsComment(subData));
						buffer.append(System.lineSeparator());
					}
					buffer.append(tabs);
					buffer.append("\t");

					if (!isArrayMember)
					{
						buffer.append(".");
						buffer.append(subData.BaseData.getFieldName());
						buffer.append(" = ");
					}
					if (subData.ChildDataDescriptions.size() > 0)
					{
						if (!isArrayMember)
						{
							buffer.append(System.lineSeparator());
							buffer.append(tabs);
							buffer.append("\t");
						}
						buffer.append("{");
						buffer.append(System.lineSeparator());
						buffer.append(subData.GetCCode(nextRecursionDepth, isArrayMember));
						//buffer.append(tabs);
						//buffer.append("}");
					}
					else
					{
						buffer.append(ChildDataDescriptions.get(scNum).GetCCode(nextRecursionDepth, isArrayMember));
					}
					if (scNum < (subComponentCount - 1))
					{
						buffer.append(",");
					}
					
					buffer.append(System.lineSeparator());
				}
				buffer.append(tabs);
				buffer.append("}");
				if (recursionDepth == 0)
				{
					buffer.append(";");
				}				
				//buffer.append(System.lineSeparator());
			}
			else
			{
				if (recursionDepth > 0)
				{
					buffer.append(u.GetCValueForAssignment(BaseDataType, Value));
				}
			}

			if (recursionDepth == 0)
			{
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: END C CODE");
				buffer.append(System.lineSeparator());
				buffer.append("// TDR: END GLOBAL VARIABLE");
				buffer.append(System.lineSeparator());
			}
			
			return buffer.toString();
		}
	}
	
	/* 
	public class DataValue
	{
		
		public List<DataValue> SubValues;
	}
	*/

	@Override
	public void run() throws Exception {
		IngredientDescription[] ingredients = getIngredientDescriptions();
		for (IngredientDescription ingredient : ingredients) {
			state.addParameter(ingredient.getID(), ingredient.getLabel(), ingredient.getType(),
				ingredient.getDefaultValue());
		}
		if (!state.displayParameterGatherer("Script Options")) {
			return;
		}
		


		LocalUtility u = new LocalUtility();
		File outputFile = (File) state.getEnvironmentVar("OutputFile");
		long minAddress = Long.decode(String.valueOf(state.getEnvironmentVar("MinimumAddress")));
		long maxAddress = Long.decode(String.valueOf(state.getEnvironmentVar("MaximumAddress")));
		int maxRecursionDepth = (Integer)state.getEnvironmentVar("MaximumRecusrion");
		
		// 0 == debug
		// 1 == output C code
		int mode = 1;
		String regexLabel = (String)state.getEnvironmentVar("RegexLabel");
		String regexDataType = (String)state.getEnvironmentVar("RegexDataTypeName");
		boolean collapseStringRefs = true;
		int csrParam = (Integer)state.getEnvironmentVar("CollapseStringRefs");
		if (csrParam == 0)
		{
			collapseStringRefs = false;
		}
			
		FileWriter output = new FileWriter(outputFile);
		BufferedWriter writer = new BufferedWriter(output);

		Data data = getFirstData();
		HashMap<Long, List<Long>> collapsedReferencesMap = new HashMap<>();
		List<DataDescription> allData = new ArrayList<DataDescription>();

		while (true)
		{
			if (monitor.isCancelled()) {
				break;
			}

			if (data == null) {
				break;
			}
			
			DataDescription dd = getDataDescription(data, minAddress, maxAddress, 0, maxRecursionDepth, collapseStringRefs, collapsedReferencesMap, false);
			if (dd != null)
			{
				allData.add(dd);
			}
			
			data = getDataAfter(data);
		}
		
		int numDDs = allData.size();
		for (int i = 0; i < numDDs; i++)
		{
			if (monitor.isCancelled()) {
				break;
			}
			
			DataDescription dd = allData.get(i);
			
			if (collapseStringRefs)
			{
				long dataAddress = dd.Address.getOffset();
				if (collapsedReferencesMap.keySet().contains(dataAddress))
				{
					String dDesc = dd.GetBestName() + " (" + u.GetShortStats(dd) + ")";
					List<Long> refsFrom = collapsedReferencesMap.get(dataAddress);
					boolean hasUnhandledReferences = false;
					for (int refNum = 0; refNum < dd.ReferencesTo.length; refNum++)
					{
						long checkRef = dd.ReferencesTo[refNum].getFromAddress().getOffset();
						if (!refsFrom.contains(checkRef))
						{
							hasUnhandledReferences = true;
							writer.write("// uncollapsed reference to " + u.LongAsHexString(dataAddress) + " at " + u.LongAsHexString(checkRef));
							writer.newLine();
						}
					}
					if (hasUnhandledReferences)
					{
						writer.write("// did not remove data for " + dDesc + ", as not all references to it were collapsed.");
						writer.newLine();
					}
					else
					{
						writer.write("// removed data for " + dDesc + " as all references to it were collapsed.");
						writer.newLine();
						dd = null;
					}
				}
			}
			
			if (dd != null)
			{
				
				if (mode == 0)
				{
					writer.write(dd.GetTabDelimitedDescription(0));
				}
				if (mode == 1)
				{
					writer.write(dd.GetCCode(0, false));
				}
				writer.newLine();
			}

		}
		
		writer.close();
	}
	
	protected String getDataValueAsString(Data data, DataType dt, boolean collapseStringRefs, HashMap<Long, List<Long>> collapsedReferencesMap, boolean charsAsPrintable)
	{
		boolean isPointer = false;
		LocalUtility u = new LocalUtility();
		
		if (data == null)
		{
			return "null";
		}
		
		String dataTypeName = dt.getName();
		if (dataTypeName.contains("*"))
		{
			isPointer = true;
		}
		if (dataTypeName.equals("pointer"))
		{
			isPointer = true;
		}
		
		if (!isPointer)
		{
			// should do something better with this
			boolean handleAsString = false;
			Object dv = data.getValue();
			
			if (dt.getName().equals("string"))
			{
				handleAsString = true;
			}
			
			if (handleAsString)
			{
				if (dv == null)
				{
					return "null";
				}
				else
				{
					return "\"" + u.GetEscapedString(String.valueOf(dv)) + "\"";
				}
			}
			
			if (u.CharTypeNames.contains(dt.getName()))
			{
				if (charsAsPrintable)
				{
					String valueString = "\\x00";
					if (dv != null)
					{
						valueString = u.GetEscapedChar((byte)u.GetCharNumFromData(data));
					}
					return "'" + valueString + "'";
				}
				else
				{
					return u.ByteAsHexString((byte)u.GetCharNumFromData(data));
				}
			}
			
			return String.valueOf(data.getValue());
		}

		String referencedItemName = "";
	
		long pointerAddress = 0;
		try
		{
			pointerAddress = Long.parseLong(data.getValue().toString(), 16);
		}
		catch (Exception e)
		{
			pointerAddress = 0;
		}
		String pointerDescription = u.LongAsHexString(pointerAddress);
			
		if (pointerAddress > 0)
		{
			referencedItemName = u.GetLabelOrDynamicName(toAddr(pointerAddress));
			
			if ((referencedItemName != null) && (!referencedItemName.equals("")))
			{
				referencedItemName = u.GetSymbolNameWithoutBadChars(referencedItemName);
				pointerDescription = "&" + referencedItemName;
			}
			if (collapseStringRefs)
			{
				String originalPointerDescription = pointerDescription;
				try
				{
					Data d = getDataAt(toAddr(pointerAddress));
					DataType ddt = d.getDataType();
					if ((d != null) && (ddt != null))
					{
						try
						{
							String pointerReferencedType = ddt.getName();
							boolean handleAsString = false;
							if (u.StringTypeNames.contains(pointerReferencedType))
							{
								handleAsString = true;
							}
							if (handleAsString)
							{
								pointerDescription = getDataValueAsString(d, ddt, collapseStringRefs, collapsedReferencesMap, false) + " /* collapsed from " + originalPointerDescription + " */";
								List<Long> refsToValue = new ArrayList<Long>();
								if (collapsedReferencesMap.keySet().contains(pointerAddress))
								{
									refsToValue = collapsedReferencesMap.get(pointerAddress);
								}
								refsToValue.add(data.getMinAddress().getOffset());
								collapsedReferencesMap.put(pointerAddress, refsToValue);
							}
						}
						catch (Exception e)
						{
							// use default method from above (&label)
							pointerDescription = originalPointerDescription;
						}
					}
				}
				catch (Exception e)
				{
					// use default method from above (&label)
					pointerDescription = originalPointerDescription;
				}
			}
		}
		else
		{
			java.lang.Object dv = data.getValue();
			if (dv == null)
			{
				pointerDescription = null;
			}
			else
			{
				pointerDescription = dv.toString();
			}
		}
		return pointerDescription;
	}
	
	protected DataDescription getDataDescription(Data data, long minAddress, long maxAddress, int recursionDepth, int maxRecursionDepth, boolean collapseStringRefs, HashMap<Long, List<Long>> collapsedReferencesMap, boolean charsAsPrintable)
	{
		LocalUtility u = new LocalUtility();
		if (recursionDepth >= maxRecursionDepth)
		{
			println("Error: hit recursion depth limit of " + String.valueOf(recursionDepth) + " while traversing this item");
			return null;
		}

		long dataOffset = data.getMinAddress().getOffset();
		if ((dataOffset < minAddress) || (dataOffset > maxAddress))
		{
			return null;
		}
		
		DataDescription result = new DataDescription();
		result.BaseData = data;
		result.BaseDataType = data.getDataType();
		result.Address = data.getMinAddress();
		result.Length = result.BaseDataType.getLength();
		SymbolTable symbolTable = currentProgram.getSymbolTable();
		result.Symbols = symbolTable.getSymbols(result.Address);	
		result.PrimarySymbol = symbolTable.getPrimarySymbol(result.Address);	
		result.DynamicName = SymbolUtilities.getDynamicName​(currentProgram, result.Address);
		result.ReferencesTo = getReferencesTo(toAddr(dataOffset));
		result.ReferencesFrom = getReferencesFrom(toAddr(dataOffset));
		
		result.Value = getDataValueAsString(result.BaseData, result.BaseDataType, collapseStringRefs, collapsedReferencesMap, charsAsPrintable);

		int subComponentCount = result.BaseData.getNumComponents();
		if (subComponentCount > 0)
		{
			boolean subCharsAsPrintable = false;
			if (result.BaseDataType.getName().contains("char") && result.BaseDataType.getName().contains("[") && (!result.BaseDataType.getName().contains("*")))
			{
				subCharsAsPrintable = true;
				// except if the array has more than n non-printable characters
				List<Data> subList = new ArrayList<Data>();
				for (int scNum = 0; scNum < subComponentCount; scNum++)
				{
					subList.add(result.BaseData.getComponent(scNum));
				}
				int numNonPrintableChars = 0;
				try
				{
					numNonPrintableChars = u.GetCountOfNonPrintableCharactersInCharArray(subList, false);
				}
				catch (Exception e)
				{
					numNonPrintableChars = u.MaximumNonPrintableCharactersInCharArray + 1;
					println(e.toString() + " thrown while getting the number of non-printable chars in the subcomponents of the data at " + u.LongAsHexString(result.Address.getOffset()) + ": " + e.getMessage());
				}
				if (numNonPrintableChars > u.MaximumNonPrintableCharactersInCharArray)
				{
					subCharsAsPrintable = false;
				}
			}
			for (int scNum = 0; scNum < subComponentCount; scNum++)
			{
				DataDescription newSub = getDataDescription(result.BaseData.getComponent(scNum), minAddress, maxAddress, recursionDepth + 1, maxRecursionDepth, collapseStringRefs, collapsedReferencesMap, subCharsAsPrintable);
				result.ChildDataDescriptions.add(newSub);
			}
		}
		return result;
	}

	@Override
	public IngredientDescription[] getIngredientDescriptions()
	{
		// note: as of this writing, trying to use GatherParamPanel.ADDRESS results in a null pointer exception when reading the value
		IngredientDescription[] retVal = new IngredientDescription[]
		{
			new IngredientDescription("OutputFile", "Output File", GatherParamPanel.FILE, "%EXPORTED_DATA_C_CODE_FILE%"),
			new IngredientDescription("MinimumAddress", "Minimum Address for Initial Pass", GatherParamPanel.STRING, "%PS_PRIMARY_MEMORY_BEGIN%"),
			new IngredientDescription("MaximumAddress", "Maximum Address for Initial Pass", GatherParamPanel.STRING, "%PS_PRIMARY_MEMORY_END%"),
			new IngredientDescription("MaximumRecusrion", "Maximum Recursion Depth", GatherParamPanel.INTEGER, 20),
			new IngredientDescription("RegexLabel", "Label Filter Regex", GatherParamPanel.STRING, ".*"),
			new IngredientDescription("RegexDataTypeName", "Data Type Name Filter Regex", GatherParamPanel.STRING, ".*"),
			new IngredientDescription("CollapseStringRefs", "Collapse String References (0 = false, 1 = true)", GatherParamPanel.INTEGER, 1)
		};
		return retVal;
	}
	
}
