/*
 * Copyright (c) 2013-2021 Chris Newland.
 * Licensed under https://github.com/AdoptOpenJDK/jitwatch/blob/master/LICENSE-BSD
 * Instructions: https://github.com/AdoptOpenJDK/jitwatch/wiki
 */
package org.adoptopenjdk.jitwatch.util;

import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.ATTR_ARGUMENTS;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.ATTR_HOLDER;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.ATTR_NAME;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.ATTR_RETURN;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.ATTR_STAMP;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.ATTR_STAMP_COMPLETED;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_CLOSE_ANGLE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_COLON;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_COMMA;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_DOT;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_OBJECT_REF;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_OPEN_ANGLE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_OPEN_SQUARE_BRACKET;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_QUOTE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_SEMICOLON;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_SLASH;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_SPACE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.DEBUG_LOGGING;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.DEBUG_LOGGING_OVC;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.DEBUG_LOGGING_SIG_MATCH;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_ARRAY_BRACKET_PAIR;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_CLASS;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_CLASS_AUTOGENERATED_LAMBDA;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_CLOSE_ANGLE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_CLOSE_PARENTHESES;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_DOT;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_DOUBLE_QUOTE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_EMPTY;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_NEWLINE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_OBJECT_ARRAY_DEF;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_OPEN_ANGLE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_OPEN_PARENTHESES;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_OPEN_SQUARE_BRACKET;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_PACKAGE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_SEMICOLON;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_SLASH;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_SPACE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TYPE_NAME_BOOLEAN;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TYPE_NAME_BYTE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TYPE_NAME_CHARACTER;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TYPE_NAME_DOUBLE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TYPE_NAME_FLOAT;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TYPE_NAME_INTEGER;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TYPE_NAME_LONG;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TYPE_NAME_SHORT;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TYPE_NAME_VOID;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_VARARGS_DOTS;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_HEX_PREFIX;

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.adoptopenjdk.jitwatch.core.JITWatchConstants;
import org.adoptopenjdk.jitwatch.model.IMetaMember;
import org.adoptopenjdk.jitwatch.model.IParseDictionary;
import org.adoptopenjdk.jitwatch.model.IReadOnlyJITDataModel;
import org.adoptopenjdk.jitwatch.model.LogParseException;
import org.adoptopenjdk.jitwatch.model.MemberSignatureParts;
import org.adoptopenjdk.jitwatch.model.MetaClass;
import org.adoptopenjdk.jitwatch.model.PackageManager;
import org.adoptopenjdk.jitwatch.model.Tag;
import org.adoptopenjdk.jitwatch.model.bytecode.BytecodeInstruction;
import com.chrisnewland.freelogj.Logger;
import com.chrisnewland.freelogj.LoggerFactory;

public final class ParseUtil
{
	private static final Logger logger = LoggerFactory.getLogger(ParseUtil.class);

	// class<SPACE>METHOD<SPACE>(PARAMS)RETURN

	// http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.2
	public static String CLASS_NAME_REGEX_GROUP = "([\\p{L}0-9$=_{};\\.\\[/<>]+)";
	public static String METHOD_NAME_REGEX_GROUP = "([^;\\[/]+)";

	public static String PARAM_REGEX_GROUP = "(\\(.*\\))";
	public static String RETURN_REGEX_GROUP = "(.*)";

	private static final Pattern PATTERN_LOG_SIGNATURE = Pattern
																.compile("^" + CLASS_NAME_REGEX_GROUP + S_SPACE
																		+ METHOD_NAME_REGEX_GROUP + S_SPACE + PARAM_REGEX_GROUP
																		+ RETURN_REGEX_GROUP);

	public static final char TYPE_SHORT = 'S';
	public static final char TYPE_CHARACTER = 'C';
	public static final char TYPE_BYTE = 'B';
	public static final char TYPE_VOID = 'V';
	public static final char TYPE_LONG = 'J';
	public static final char TYPE_DOUBLE = 'D';
	public static final char TYPE_BOOLEAN = 'Z';
	public static final char TYPE_INTEGER = 'I';
	public static final char TYPE_FLOAT = 'F';

	private ParseUtil()
	{
	}

	public static long parseHexAddress(String str)
	{
		if (str.startsWith(S_HEX_PREFIX))
		{
			str = str.substring(S_HEX_PREFIX.length());
		}

		return Long.parseLong(str, 16);
	}

	public static long parseStamp(String stamp)
	{
		long result = 0;

		if (stamp != null)
		{
			double number = parseLocaleSafeDouble(stamp);

			result = (long) (number * 1000);
		}
		else
		{
			logger.warn("Could not parse null stamp");
			Thread.dumpStack();
		}

		return result;
	}

	public static long parseStampFromTag(Tag tag)
	{
		String attrValue = tag.getAttributes().get(ATTR_STAMP);

		long result = 0;

		if (attrValue != null)
		{
			result = parseStamp(attrValue);
		}
		else
		{
			logger.error("attribute {} missing from tag {}", ATTR_STAMP, tag.toString(true));
		}

		return result;
	}

	public static long parseLongAttributeFromTag(Tag tag, String attrName)
	{
		String attrValue = tag.getAttributes().get(attrName);

		long result = 0;

		if (attrValue != null)
		{
			result = Long.parseLong(attrValue);
		}
		else
		{
			logger.error("attribute {} missing from tag {}", attrName, tag.toString(true));
		}

		return result;
	}

	public static long getStamp(Map<String, String> attrs)
	{
		long result = 0;

		if (attrs.containsKey(ATTR_STAMP_COMPLETED))
		{
			result = parseStamp(attrs.get(ATTR_STAMP_COMPLETED));
		}
		else if (attrs.containsKey(ATTR_STAMP))
		{
			result = parseStamp(attrs.get(ATTR_STAMP));
		}

		return result;
	}

	public static double parseLocaleSafeDouble(String str)
	{
		NumberFormat nf = NumberFormat.getInstance(Locale.getDefault());

		double result = 0;

		try
		{
			result = nf.parse(str.trim()).doubleValue();
		}
		catch (ParseException pe)
		{
			logger.warn("Could not parse {} as a Double", str, pe);
		}

		return result;
	}

	public static Class<?> getPrimitiveClass(char c)
	{
		switch (c)
		{
		case TYPE_SHORT:
			return Short.TYPE;
		case TYPE_CHARACTER:
			return Character.TYPE;
		case TYPE_BYTE:
			return Byte.TYPE;
		case TYPE_VOID:
			return Void.TYPE;
		case TYPE_LONG:
			return Long.TYPE;
		case TYPE_DOUBLE:
			return Double.TYPE;
		case TYPE_BOOLEAN:
			return Boolean.TYPE;
		case TYPE_INTEGER:
			return Integer.TYPE;
		case TYPE_FLOAT:
			return Float.TYPE;
		}

		throw new RuntimeException("Unknown class for " + c);
	}

	public static int getArrayDepth(String input)
	{
		int result = 0;

		for (int i = 0; i < input.length(); i++)
		{
			char c = input.charAt(i);

			if (c == C_OPEN_SQUARE_BRACKET)
			{
				result++;
			}
			else
			{
				break;
			}
		}

		return result;
	}

	// I => int
	// [C => char[]
	// [[I => int[][]
	// [Ljava.lang.Object; => java.lang.Object[]
	public static String expandParameterType(String name)
	{
		StringBuilder builder = new StringBuilder();

		int arrayDepth = getArrayDepth(name);

		int nameLengthWithoutArrayDepth = name.length() - arrayDepth;

		int nameStart = arrayDepth;

		if (nameLengthWithoutArrayDepth == 1)
		{
			char c = name.charAt(nameStart);

			switch (c)
			{
			case TYPE_SHORT:
				builder.append(S_TYPE_NAME_SHORT);
				break;
			case TYPE_CHARACTER:
				builder.append(S_TYPE_NAME_CHARACTER);
				break;
			case TYPE_BYTE:
				builder.append(S_TYPE_NAME_BYTE);
				break;
			case TYPE_LONG:
				builder.append(S_TYPE_NAME_LONG);
				break;
			case TYPE_DOUBLE:
				builder.append(S_TYPE_NAME_DOUBLE);
				break;
			case TYPE_BOOLEAN:
				builder.append(S_TYPE_NAME_BOOLEAN);
				break;
			case TYPE_INTEGER:
				builder.append(S_TYPE_NAME_INTEGER);
				break;
			case TYPE_FLOAT:
				builder.append(S_TYPE_NAME_FLOAT);
				break;
			}
		}
		else if (name.charAt(nameStart) == C_OBJECT_REF && name.endsWith(S_SEMICOLON))
		{
			builder.append(name.substring(nameStart + 1, name.length() - 1));
		}
		else
		{
			builder.append(name.substring(nameStart));
		}

		for (int i = 0; i < arrayDepth; i++)
		{
			builder.append(S_ARRAY_BRACKET_PAIR);
		}

		return builder.toString();
	}

	public static String[] splitLogSignatureWithRegex(final String logSignature) throws LogParseException
	{
		String sig = logSignature;

		sig = StringUtil.replaceXMLEntities(sig);

		Matcher matcher = PATTERN_LOG_SIGNATURE.matcher(sig);

		if (matcher.find())
		{
			String className = matcher.group(1);
			String methodName = matcher.group(2);
			String paramTypes = matcher.group(3).replace(S_OPEN_PARENTHESES, S_EMPTY).replace(S_CLOSE_PARENTHESES, S_EMPTY);
			String returnType = matcher.group(4);

			return new String[] { className, methodName, paramTypes, returnType };
		}

		logger.debug("Could not apply {} to {}", PATTERN_LOG_SIGNATURE, logSignature);

		throw new LogParseException("Could not split signature with regex: '" + logSignature + C_QUOTE);
	}

	public static IMetaMember findMemberWithSignature(IReadOnlyJITDataModel model, String logSignature) throws LogParseException
	{
		IMetaMember metaMember = null;

		if (logSignature != null)
		{
			MemberSignatureParts msp = MemberSignatureParts.fromLogCompilationSignature(logSignature);

			metaMember = model.findMetaMember(msp);

			if (metaMember == null)
			{
				throw new LogParseException("MetaMember not found for " + logSignature);
			}
		}

		return metaMember;
	}

	public static Class<?>[] getClassTypes(String typesString) throws LogParseException
	{
		List<Class<?>> classes = null;

		try
		{
			classes = findClassesForTypeString(typesString);
		}
		catch (Throwable t)
		{
			throw new LogParseException("Could not parse types: " + typesString, t);
		}

		return classes.toArray(new Class<?>[classes.size()]);
	}

	public static Class<?> findClassForLogCompilationParameter(String param) throws ClassNotFoundException
	{
		StringBuilder builder = new StringBuilder();

		if (isPrimitive(param))
		{
			return classForPrimitive(param);
		}
		else
		{
			int arrayBracketCount = getArrayBracketCount(param);

			if (param.contains(S_CLOSE_ANGLE))
			{
				param = stripGenerics(param);
			}

			if (arrayBracketCount == 0)
			{
				if (param.endsWith(S_VARARGS_DOTS))
				{
					String partBeforeDots = param.substring(0, param.length() - S_VARARGS_DOTS.length());

					if (isPrimitive(partBeforeDots))
					{
						builder.append(S_OPEN_ANGLE).append(classForPrimitive(partBeforeDots));
					}
					else
					{
						builder.append(S_OBJECT_ARRAY_DEF).append(partBeforeDots);
						builder.append(C_SEMICOLON);
					}
				}
				else
				{
					builder.append(param);
				}
			}
			else
			{
				int arrayBracketChars = 2 * arrayBracketCount;

				String partBeforeArrayBrackets = param.substring(0, param.length() - arrayBracketChars);

				for (int i = 0; i < arrayBracketCount - 1; i++)
				{
					builder.append(C_OPEN_SQUARE_BRACKET);
				}

				if (isPrimitive(partBeforeArrayBrackets))
				{
					builder.append(C_OPEN_SQUARE_BRACKET);

					builder.append(getClassTypeCharForPrimitiveTypeString(partBeforeArrayBrackets));
				}
				else
				{
					builder.append(S_OBJECT_ARRAY_DEF);

					builder.append(param);

					builder.delete(builder.length() - arrayBracketChars, builder.length());

					builder.append(C_SEMICOLON);
				}
			}

			return ClassUtil.loadClassWithoutInitialising(builder.toString());
		}
	}

	public static String stripGenerics(String param)
	{
		String result = param;

		if (param != null)
		{
			int firstOpenAngle = param.indexOf(C_OPEN_ANGLE);
			int lastCloseAngle = param.lastIndexOf(C_CLOSE_ANGLE);

			if (firstOpenAngle != -1 && lastCloseAngle != -1 && firstOpenAngle < lastCloseAngle)
			{
				result = param.substring(0, firstOpenAngle) + param.substring(lastCloseAngle + 1);
			}
		}

		return result;
	}

	public static boolean paramClassesMatch(boolean memberHasVarArgs, List<Class<?>> memberParamClasses,
			List<Class<?>> signatureParamClasses, boolean matchTypesExactly)
	{
		boolean result = true;

		final int memberParamCount = memberParamClasses.size();
		final int signatureParamCount = signatureParamClasses.size();

		if (DEBUG_LOGGING_SIG_MATCH)
		{
			logger
					.debug("MemberParamCount:{} SignatureParamCount:{} varArgs:{}", memberParamCount, signatureParamCount,
							memberHasVarArgs);
		}

		if (memberParamCount == 0 && signatureParamCount == 0)
		{
			if (DEBUG_LOGGING_SIG_MATCH)
			{
				logger.debug("both have zero params");
			}

			result = true;
		}
		else if (signatureParamCount < memberParamCount)
		{
			if (DEBUG_LOGGING_SIG_MATCH)
			{
				logger.debug("signature has less params than method");
			}

			result = false;
		}
		else if (signatureParamCount > memberParamCount && !memberHasVarArgs)
		{
			if (DEBUG_LOGGING_SIG_MATCH)
			{
				logger.debug("signature has more params than non-varargs method");
			}

			result = false;
		}
		else
		{
			// signature params >= memberParams

			int memPos = 0;

			for (int sigPos = 0; sigPos < signatureParamCount; sigPos++)
			{
				Class<?> sigParamClass = signatureParamClasses.get(sigPos);

				Class<?> memParamClass = memberParamClasses.get(memPos);

				if (DEBUG_LOGGING_SIG_MATCH)
				{
					logger.debug("Comparing member param[{}] {} to sig param[{}] {}", memPos, memParamClass, sigPos, sigParamClass);
				}

				boolean classMatch = false;

				if (matchTypesExactly)
				{
					classMatch = memParamClass.equals(sigParamClass);
				}
				else
				{
					classMatch = memParamClass.isAssignableFrom(sigParamClass);
				}

				if (classMatch)
				{
					if (DEBUG_LOGGING_SIG_MATCH)
					{
						logger.debug("{} equals/isAssignableFrom {}", memParamClass, sigParamClass);
					}
				}
				else
				{

					boolean memberParamCouldBeVarArgs = false;

					boolean isLastParameter = (memPos == memberParamCount - 1);

					if (memberHasVarArgs && isLastParameter)
					{
						memberParamCouldBeVarArgs = true;
					}

					if (memberParamCouldBeVarArgs)
					{
						// check assignable
						Class<?> componentType = memParamClass.getComponentType();

						if (componentType != null && componentType.isAssignableFrom(sigParamClass))
						{
							if (DEBUG_LOGGING_SIG_MATCH)
							{
								logger.debug("vararg member param {} equals/isAssignableFrom {}", memParamClass, sigParamClass);
							}
						}
						else
						{
							result = false;
							break;
						}
					}
					else
					{
						result = false;
						break;
					}

				} // if classMatch

				memPos++;

				if (memPos >= memberParamCount)
				{
					memPos = memberParamCount - 1;
				}

			} // for
		}

		return result;
	}

	public static boolean typeIsVarArgs(String type)
	{
		return type != null && type.endsWith(S_VARARGS_DOTS);
	}

	public static char getClassTypeCharForPrimitiveTypeString(String type)
	{
		switch (type)
		{
		case S_TYPE_NAME_INTEGER:
			return TYPE_INTEGER;
		case S_TYPE_NAME_BOOLEAN:
			return TYPE_BOOLEAN;
		case S_TYPE_NAME_LONG:
			return TYPE_LONG;
		case S_TYPE_NAME_DOUBLE:
			return TYPE_DOUBLE;
		case S_TYPE_NAME_FLOAT:
			return TYPE_FLOAT;
		case S_TYPE_NAME_SHORT:
			return TYPE_SHORT;
		case S_TYPE_NAME_BYTE:
			return TYPE_BYTE;
		case S_TYPE_NAME_CHARACTER:
			return TYPE_CHARACTER;
		case S_TYPE_NAME_VOID:
			return TYPE_VOID;
		}

		throw new RuntimeException(type + " is not a primitive type");
	}

	public static boolean isPrimitive(String type)
	{
		boolean result = false;

		if (type != null)
		{
			switch (type)
			{
			case S_TYPE_NAME_INTEGER:
			case S_TYPE_NAME_BOOLEAN:
			case S_TYPE_NAME_LONG:
			case S_TYPE_NAME_DOUBLE:
			case S_TYPE_NAME_FLOAT:
			case S_TYPE_NAME_SHORT:
			case S_TYPE_NAME_BYTE:
			case S_TYPE_NAME_CHARACTER:
			case S_TYPE_NAME_VOID:
				result = true;
			}
		}

		return result;
	}

	public static Class<?> classForPrimitive(String primitiveType)
	{
		if (primitiveType != null)
		{
			switch (primitiveType)
			{
			case S_TYPE_NAME_INTEGER:
				return int.class;
			case S_TYPE_NAME_BOOLEAN:
				return boolean.class;
			case S_TYPE_NAME_LONG:
				return long.class;
			case S_TYPE_NAME_DOUBLE:
				return double.class;
			case S_TYPE_NAME_FLOAT:
				return float.class;
			case S_TYPE_NAME_SHORT:
				return short.class;
			case S_TYPE_NAME_BYTE:
				return byte.class;
			case S_TYPE_NAME_CHARACTER:
				return char.class;
			case S_TYPE_NAME_VOID:
				return void.class;
			}
		}

		throw new RuntimeException(primitiveType + " is not a primitive type");
	}

	public static int getArrayBracketCount(String param)
	{
		int count = 0;

		if (param != null)
		{
			int index = param.indexOf(S_ARRAY_BRACKET_PAIR, 0);

			while (index != -1)
			{
				count++;

				index = param.indexOf(S_ARRAY_BRACKET_PAIR, index + 2);
			}
		}

		return count;
	}

	public static List<String> parseTypeString(final String typesString)
	{
		List<String> result = new ArrayList<>();

		String toParse = typesString.replace(C_SLASH, C_DOT);

		int pos = 0;

		StringBuilder builder = new StringBuilder();

		final int stringLen = toParse.length();

		while (pos < stringLen)
		{
			char c = toParse.charAt(pos);

			switch (c)
			{
			case C_OPEN_SQUARE_BRACKET:
				// Could be
				// [Ljava.lang.String; Object array
				// [I primitive array
				// [..[I multidimensional primitive array
				// [..[Ljava.lang.String multidimensional Object array
				builder.delete(0, builder.length());
				builder.append(c);
				pos++;
				c = toParse.charAt(pos);

				while (c == C_OPEN_SQUARE_BRACKET)
				{
					builder.append(c);
					pos++;
					c = toParse.charAt(pos);
				}

				if (c == C_OBJECT_REF)
				{
					// array of ref type
					while (pos < stringLen)
					{
						c = toParse.charAt(pos++);
						builder.append(c);

						if (c == C_SEMICOLON)
						{
							break;
						}
					}
				}
				else
				{
					// array of primitive
					builder.append(c);
					pos++;
				}

				result.add(builder.toString());
				builder.delete(0, builder.length());
				break;
			case C_OBJECT_REF:
				// ref type
				while (pos < stringLen - 1)
				{
					pos++;
					c = toParse.charAt(pos);

					if (c == C_SEMICOLON)
					{
						pos++;
						break;
					}

					builder.append(c);
				}
				result.add(builder.toString());
				builder.delete(0, builder.length());
				break;
			default:
				// primitive
				result.add(Character.toString(c));
				pos++;

			} // end switch

		} // end while

		return result;
	}

	public static boolean looksLikeSyntheticBridgeConstructorParam(String className)
	{
		return className != null && className.endsWith("$1");
	}

	/*
	 * Converts (III[Ljava.lang.String;) into a list of Class<?>
	 */
	public static List<Class<?>> findClassesForTypeString(final String typesString) throws ClassNotFoundException
	{
		List<Class<?>> result = new ArrayList<>();

		List<String> typeNames = parseTypeString(typesString);

		for (String typeName : typeNames)
		{
			Class<?> clazz = null;

			if (typeName.length() == 1)
			{
				clazz = getPrimitiveClass(typeName.charAt(0));
			}
			else
			{
				if (looksLikeSyntheticBridgeConstructorParam(typeName))
				{
					logger.debug("Not attempting to classload synthetic bridge constructor arg: {}", typeName);
					continue;
				}

				clazz = ClassUtil.loadClassWithoutInitialising(typeName);
			}

			result.add(clazz);
		}

		return result;
	}

	public static String findBestMatchForMemberSignature(IMetaMember member, List<String> lines)
	{
		String match = null;

		if (lines != null)
		{
			int index = findBestLineMatchForMemberSignature(member, lines);

			if (index > 0 && index < lines.size())
			{
				match = lines.get(index);
			}
		}

		return match;
	}

	public static int findBestLineMatchForMemberSignature(IMetaMember member, List<String> lines)
	{
		int bestScoreLine = 0;

		if (lines != null)
		{
			String memberName = member.getMemberName();
			int modifier = member.getModifier();
			String returnTypeName = member.getReturnTypeName();
			String[] paramTypeNames = member.getParamTypeNames();

			int bestScore = 0;

			for (int i = 0; i < lines.size(); i++)
			{
				String line = lines.get(i);

				int score = 0;

				if (line.contains(memberName))
				{
					if (DEBUG_LOGGING_SIG_MATCH)
					{
						logger.debug("Comparing {} with {}", line, member);
					}

					MemberSignatureParts msp = MemberSignatureParts
																	.fromBytecodeSignature(
																			member.getMetaClass().getFullyQualifiedName(), line);

					if (!memberName.equals(msp.getMemberName()))
					{
						continue;
					}

					// modifiers matched
					if (msp.getModifier() != modifier)
					{
						continue;
					}

					List<String> mspParamTypes = msp.getParamTypes();

					if (mspParamTypes.size() != paramTypeNames.length)
					{
						continue;
					}

					int pos = 0;

					for (String memberParamType : paramTypeNames)
					{
						String mspParamType = msp.getParamTypes().get(pos++);

						if (compareTypeEquality(memberParamType, mspParamType, msp.getGenerics()))
						{
							score++;
						}
					}

					// return type matched
					if (compareTypeEquality(returnTypeName, msp.getReturnType(), msp.getGenerics()))
					{
						score++;
					}

					if (score > bestScore)
					{
						bestScoreLine = i;
						bestScore = score;
					}
				}
			}
		}

		return bestScoreLine;
	}

	private static boolean compareTypeEquality(String memberTypeName, String inMspTypeName, Map<String, String> genericsMap)
	{
		String mspTypeName = inMspTypeName;

		if (memberTypeName != null && memberTypeName.equals(mspTypeName))
		{
			return true;
		}
		else if (mspTypeName != null)
		{
			// Substitute generics to match with non-generic signature
			// public static <T extends java.lang.Object, U extends
			// java.lang.Object> T[] copyOf(U[], int, java.lang.Class<? extends
			// T[]>)";
			// U[] -> java.lang.Object[]

			String mspTypeNameWithoutArray = getParamTypeWithoutArrayBrackets(mspTypeName);

			String genericSubstitution = genericsMap.get(mspTypeNameWithoutArray);

			if (genericSubstitution != null)
			{
				mspTypeName = mspTypeName.replace(mspTypeNameWithoutArray, genericSubstitution);

				if (memberTypeName != null && memberTypeName.equals(mspTypeName))
				{
					return true;
				}
			}
		}

		return false;
	}

	public static String getParamTypeWithoutArrayBrackets(String paramType)
	{
		int bracketsIndex = paramType.indexOf(S_ARRAY_BRACKET_PAIR);

		if (bracketsIndex != -1)
		{
			return paramType.substring(0, bracketsIndex);
		}
		else
		{
			return paramType;
		}
	}

	public static String getMethodTagReturn(Tag methodTag, IParseDictionary parseDictionary)
	{
		String returnTypeId = methodTag.getAttributes().get(ATTR_RETURN);

		String returnType = lookupType(returnTypeId, parseDictionary);

		return returnType;
	}

	public static List<String> getMethodTagArguments(Tag methodTag, IParseDictionary parseDictionary)
	{
		List<String> result = new ArrayList<>();

		String arguments = methodTag.getAttributes().get(ATTR_ARGUMENTS);

		if (arguments != null)
		{
			String[] typeIDs = arguments.split(S_SPACE);

			for (String typeID : typeIDs)
			{
				String typeName = lookupType(typeID, parseDictionary);

				if (ParseUtil.looksLikeSyntheticBridgeConstructorParam(typeName))
				{
					logger.debug("Ignoring synthetic bridge constructor arg from comparison: {}", typeName);
					continue;
				}
				result.add(typeName);
			}
		}

		return result;
	}

	public static String getMethodName(String methodID, IParseDictionary parseDictionary)
	{
		String result = null;

		Tag methodTag = parseDictionary.getMethod(methodID);

		if (methodTag != null)
		{
			String methodName = methodTag.getAttributes().get(ATTR_NAME);

			result = StringUtil.replaceXMLEntities(methodName);
		}

		return result;
	}

	public static String lookupMetaClassName(String methodId, IParseDictionary parseDictionary)
	{
		String metaClassName = null;

		Tag methodTag = parseDictionary.getMethod(methodId);

		if (methodTag != null)
		{
			Map<String, String> methodTagAttributes = methodTag.getAttributes();

			String klassId = methodTagAttributes.get(ATTR_HOLDER);

			Tag klassTag = parseDictionary.getKlass(klassId);

			metaClassName = klassTag.getAttributes().get(ATTR_NAME).replace(S_SLASH, S_DOT);
		}

		return metaClassName;
	}

	public static String lookupMethodName(String methodId, IParseDictionary parseDictionary)
	{
		String methodName = null;

		Tag methodTag = parseDictionary.getMethod(methodId);

		if (methodTag != null)
		{
			Map<String, String> methodTagAttributes = methodTag.getAttributes();

			methodName = methodTagAttributes.get(ATTR_NAME);

			methodName = StringUtil.replaceXMLEntities(methodName);
		}

		return methodName;
	}

	public static IMetaMember lookupMember(String methodId, IParseDictionary parseDictionary, IReadOnlyJITDataModel model)
	{
		IMetaMember result = null;

		Tag methodTag = parseDictionary.getMethod(methodId);

		if (methodTag != null)
		{
			String metaClassName = lookupMetaClassName(methodId, parseDictionary);

			PackageManager pm = model.getPackageManager();

			MetaClass metaClass = pm.getMetaClass(metaClassName);

			if (metaClass == null)
			{
				metaClass = lateLoadMetaClass(model, metaClassName);
			}

			if (metaClass != null)
			{
				String methodName = lookupMethodName(methodId, parseDictionary);

				String returnType = getMethodTagReturn(methodTag, parseDictionary);

				List<String> argumentTypes = getMethodTagArguments(methodTag, parseDictionary);

				MemberSignatureParts msp = MemberSignatureParts
																.fromParts(metaClass.getFullyQualifiedName(), methodName,
																		returnType, argumentTypes);

				result = metaClass.getMemberForSignature(msp);
			}
			else if (!possibleLambdaMethod(metaClassName))
			{
				logger.error("metaClass not found: {}", metaClassName);
			}
		}

		return result;
	}

	public static MetaClass lateLoadMetaClass(IReadOnlyJITDataModel model, String metaClassName)
	{
		if (DEBUG_LOGGING)
		{
			logger.debug("metaClass not found: {}. Attempting classload", metaClassName);
		}

		MetaClass metaClass = null;

		try
		{
			Class<?> clazz = ClassUtil.loadClassWithoutInitialising(metaClassName);

			if (clazz != null)
			{
				metaClass = model.buildAndGetMetaClass(clazz);
			}
		}
		catch (ClassNotFoundException cnf)
		{
			if (!possibleLambdaMethod(metaClassName))
			{
				logger.error("ClassNotFoundException: '" + metaClassName + C_QUOTE);
			}
		}
		catch (NoClassDefFoundError ncdf)
		{
			logger.error("NoClassDefFoundError: '" + metaClassName + C_SPACE + ncdf.getMessage() + C_QUOTE);
		}
		catch (IllegalAccessError iae)
		{
			if (!isVMInternalClass(metaClassName))
			{
				logger.error("IllegalAccessError: '" + metaClassName + C_SPACE + iae.getMessage() + C_QUOTE);
			}
		}

		return metaClass;
	}

	public static boolean possibleLambdaMethod(String fqClassName)
	{
		if (fqClassName.contains(S_CLASS_AUTOGENERATED_LAMBDA))
		{
			return true;
		}
		else
		{
			for (String prefix : JITWatchConstants.getAutoGeneratedClassPrefixes())
			{
				if (fqClassName.startsWith(prefix))
				{
					return true;
				}
			}

			return false;
		}
	}

	public static boolean isVMInternalClass(String fqClassName)
	{
		return fqClassName.startsWith("org.graalvm");
	}

	public static String lookupType(String typeOrKlassID, IParseDictionary parseDictionary)
	{
		String result = null;

		boolean isType = true;

		if (typeOrKlassID != null)
		{
			Tag typeTag = parseDictionary.getType(typeOrKlassID);

			if (typeTag == null)
			{
				typeTag = parseDictionary.getKlass(typeOrKlassID);
				isType = false;
			}

			if (typeTag != null)
			{
				String typeAttrName = typeTag.getAttributes().get(ATTR_NAME);

				if (typeAttrName != null)
				{
					typeAttrName = typeAttrName.replace(S_SLASH, S_DOT);

					if (isType || typeAttrName.startsWith(S_OPEN_SQUARE_BRACKET))
					{
						result = expandParseDictionaryTypeName(typeAttrName);
					}
					else
					{
						result = typeAttrName;
					}
				}
			}
		}

		return result;
	}

	public static String expandParseDictionaryTypeName(String typeName)
	{
		String result = null;

		if (typeName != null)
		{
			result = typeName.replace(S_SLASH, S_DOT);

			result = expandParameterType(result);
		}

		return result;
	}

	public static String getPackageFromSource(String source)
	{
		String result = null;

		String[] lines = source.split(S_NEWLINE);

		for (String line : lines)
		{
			line = line.trim();

			if (line.startsWith(S_PACKAGE) && line.endsWith(S_SEMICOLON))
			{
				result = line.substring(S_PACKAGE.length(), line.length() - 1).trim();
			}
		}

		if (result == null)
		{
			result = S_EMPTY;
		}

		return result;
	}

	public static String getClassFromSource(String source)
	{
		String result = null;

		String[] lines = source.split(S_NEWLINE);

		String classToken = S_SPACE + S_CLASS + S_SPACE;

		for (String line : lines)
		{
			line = line.trim();

			int classTokenPos = line.indexOf(classToken);

			if (classTokenPos != -1)
			{
				result = line.substring(classTokenPos + classToken.length());
			}
		}

		if (result == null)
		{
			result = "";
		}

		return result;
	}

	public static String bytecodeMethodCommentToReadableString(String className, String comment)
	{
		StringBuilder builder = new StringBuilder();

		if (bytecodeMethodCommentHasNoClassPrefix(comment))
		{
			comment = className.replace(S_DOT, S_SLASH) + C_DOT + comment;
		}

		String logCompilationSignature = bytecodeCommentSignatureToLogCompilationSignature(comment);

		try
		{
			String[] parts = ParseUtil.splitLogSignatureWithRegex(logCompilationSignature);

			String fullyQualifiedClassName = parts[0];
			String memberName = parts[1];
			String paramTypes = parts[2];

			builder.append(fullyQualifiedClassName).append(S_DOT);
			builder.append(memberName).append(S_OPEN_PARENTHESES);

			List<String> paramTypeNames = parseTypeString(paramTypes);

			if (paramTypeNames.size() > 0)
			{
				for (String paramTypeName : paramTypeNames)
				{

					builder.append(expandParameterType(paramTypeName));

					builder.append(C_COMMA);
				}

				builder.deleteCharAt(builder.length() - 1);
			}

			builder.append(S_CLOSE_PARENTHESES);
		}
		catch (LogParseException e)
		{
			e.printStackTrace();
		}

		return builder.toString();
	}

	public static boolean bytecodeMethodCommentHasNoClassPrefix(String comment)
	{
		return (comment.indexOf(C_DOT) == -1);
	}

	private static String prependCurrentMember(String comment, IMetaMember member)
	{
		String currentClass = member.getMetaClass().getFullyQualifiedName();

		currentClass = currentClass.replace(C_DOT, C_SLASH);

		return currentClass + C_DOT + comment;
	}

	public static String bytecodeCommentSignatureToLogCompilationSignature(String bytcodeCommentSignature)
	{
		return bytcodeCommentSignature
										.replace(C_DOT, C_SPACE)
										.replace(C_COLON, C_SPACE)
										.replace(C_SLASH, C_DOT)
										.replace(S_DOUBLE_QUOTE, S_EMPTY);
	}

	public static IMetaMember getMemberFromBytecodeComment(IReadOnlyJITDataModel model, IMetaMember currentMember,
			BytecodeInstruction instruction) throws LogParseException
	{
		IMetaMember result = null;

		if (DEBUG_LOGGING_OVC)
		{
			logger.debug("Looking for member in {} using {}", currentMember, instruction);
		}

		if (instruction != null)
		{
			String comment = instruction.getCommentWithMemberPrefixStripped();

			if (comment != null)
			{
				if (bytecodeMethodCommentHasNoClassPrefix(comment) && currentMember != null)
				{
					comment = prependCurrentMember(comment, currentMember);
				}

				MemberSignatureParts msp = MemberSignatureParts.fromBytecodeComment(comment);

				result = model.findMetaMember(msp);
			}
		}

		return result;
	}
}
