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

import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_COLON;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_HASH;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_NEWLINE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_OPEN_ANGLE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_SEMICOLON;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.DEBUG_LOGGING_BYTECODE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_CLASSFILE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_CODE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_CONSTANT_POOL;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_EXCEPTION_TABLE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_INNERCLASSES;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_LINENUMBERTABLE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_LOCALVARIABLETABLE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_MAJOR_VERSION;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_MINOR_VERSION;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_RUNTIMEVISIBLEANNOTATIONS;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_SIGNATURE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_SOURCE_FILE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_STACKMAPTABLE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_BYTECODE_STATIC_INITIALISER_SIGNATURE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_CLOSE_BRACE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_COLON;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_COMMA;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_DEFAULT;
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_HASH;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_NEWLINE;
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 java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.adoptopenjdk.jitwatch.model.MemberSignatureParts;
import org.adoptopenjdk.jitwatch.model.MetaClass;
import org.adoptopenjdk.jitwatch.model.bytecode.BCParamConstant;
import org.adoptopenjdk.jitwatch.model.bytecode.BCParamNumeric;
import org.adoptopenjdk.jitwatch.model.bytecode.BCParamString;
import org.adoptopenjdk.jitwatch.model.bytecode.BCParamSwitch;
import org.adoptopenjdk.jitwatch.model.bytecode.BytecodeInstruction;
import org.adoptopenjdk.jitwatch.model.bytecode.ClassBC;
import org.adoptopenjdk.jitwatch.model.bytecode.ExceptionTableEntry;
import org.adoptopenjdk.jitwatch.model.bytecode.IBytecodeParam;
import org.adoptopenjdk.jitwatch.model.bytecode.InnerClassRelationship;
import org.adoptopenjdk.jitwatch.model.bytecode.LineTableEntry;
import org.adoptopenjdk.jitwatch.model.bytecode.MemberBytecode;
import org.adoptopenjdk.jitwatch.model.bytecode.Opcode;
import org.adoptopenjdk.jitwatch.model.bytecode.SourceMapper;
import org.adoptopenjdk.jitwatch.process.javap.JavapProcess;
import org.adoptopenjdk.jitwatch.process.javap.ReflectionJavap;
import org.adoptopenjdk.jitwatch.util.StringUtil;
import com.chrisnewland.freelogj.Logger;
import com.chrisnewland.freelogj.LoggerFactory;

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

	private static final Pattern PATTERN_BYTECODE_INSTRUCTION = Pattern.compile(
			"^([0-9]+):\\s([0-9a-z_]+)\\s?([#0-9a-z,\\- ]+)?\\s?\\{?\\s?(//.*)?");

	enum BytecodeSection
	{
		NONE, CONSTANT_POOL, CODE, EXCEPTIONTABLE, LINETABLE, RUNTIMEVISIBLEANNOTATIONS, LOCALVARIABLETABLE, STACKMAPTABLE, INNERCLASSES
	}

	private static final Map<String, BytecodeSection> sectionLabelMap = new HashMap<>();

	static
	{
		sectionLabelMap.put(S_BYTECODE_CONSTANT_POOL, BytecodeSection.CONSTANT_POOL);
		sectionLabelMap.put(S_BYTECODE_CODE, BytecodeSection.CODE);
		sectionLabelMap.put(S_BYTECODE_LINENUMBERTABLE, BytecodeSection.LINETABLE);
		sectionLabelMap.put(S_BYTECODE_LOCALVARIABLETABLE, BytecodeSection.LOCALVARIABLETABLE);
		sectionLabelMap.put(S_BYTECODE_RUNTIMEVISIBLEANNOTATIONS, BytecodeSection.RUNTIMEVISIBLEANNOTATIONS);
		sectionLabelMap.put(S_BYTECODE_EXCEPTION_TABLE, BytecodeSection.EXCEPTIONTABLE);
		sectionLabelMap.put(S_BYTECODE_STACKMAPTABLE, BytecodeSection.STACKMAPTABLE);
		sectionLabelMap.put(S_BYTECODE_INNERCLASSES, BytecodeSection.INNERCLASSES);
	}

	private BytecodeLoader()
	{
	}

	/*
	 * Builds a meta class from bytecode where JITDataModel.buildAndGetMetaClass
	 * fails due to NoClassDefFoundError
	 */
	public static MetaClass buildMetaClassFromClass(String fqClassName)
	{
		// TODO implement
		return null;
	}

	public static ClassBC fetchBytecodeForClass(List<String> classLocations, String fqClassName, boolean cacheBytecode)
	{
		return fetchBytecodeForClass(classLocations, fqClassName, null, cacheBytecode);
	}

	public static ClassBC fetchBytecodeForClass(List<String> classLocations, String fqClassName, Path javapPath,
			boolean cacheBytecode)
	{
		if (DEBUG_LOGGING_BYTECODE)
		{
			logger.debug("fetchBytecodeForClass: {}", fqClassName);

			logger.debug("Class locations: {}", StringUtil.listToString(classLocations));
		}

		ClassBC classBytecode = null;

		String byteCodeString = null;

		try
		{
			if (ReflectionJavap.canUseReflectionJavap())
			{
				try
				{
					byteCodeString = ReflectionJavap.getBytecode(classLocations, fqClassName);
				}
				catch (Exception e)
				{
					logger.info("Could not fetch bytecode via reflection, trying Process");

					byteCodeString = getBytecodeStringViaProcess(classLocations, fqClassName, javapPath);
				}
			}
			else
			{
				byteCodeString = getBytecodeStringViaProcess(classLocations, fqClassName, javapPath);
			}

			classBytecode = parseByteCodeFromString(fqClassName, byteCodeString, cacheBytecode);

		}
		catch (Exception e)
		{
			logger.error("Could not fetch bytecode for {}", fqClassName, e);
		}

		return classBytecode;
	}

	private static String getBytecodeStringViaProcess(List<String> classLocations, String fqClassName, Path javapPath)
			throws IOException
	{
		JavapProcess javapProcess;

		if (javapPath != null)
		{
			javapProcess = new JavapProcess(javapPath);
		}
		else
		{
			javapProcess = new JavapProcess();
		}

		Map<String, String> environment = new HashMap<>();

		javapProcess.execute(classLocations, fqClassName, environment);

		return javapProcess.getOutputStream();
	}

	private static ClassBC parseByteCodeFromString(String fqClassName, String byteCodeString, boolean cacheBytecode)
	{
		ClassBC result = null;

		if (byteCodeString != null)
		{
			String[] bytecodeLines = byteCodeString.split(S_NEWLINE);

			try
			{
				result = parse(fqClassName, bytecodeLines, cacheBytecode);
			}
			catch (Throwable t)
			{
				logger.error("Exception parsing bytecode", t);
			}
		}

		return result;
	}

	// TODO refactor this class - better stateful than all statics
	public static ClassBC parse(String fqClassName, String[] bytecodeLines, boolean cacheBytecode)
	{
		ClassBC classBytecode = new ClassBC(fqClassName);

		int pos = 0;

		StringBuilder builder = new StringBuilder();

		BytecodeSection section = BytecodeSection.NONE;

		MemberSignatureParts msp = null;

		MemberBytecode memberBytecode = null;

		while (pos < bytecodeLines.length)
		{
			String line = bytecodeLines[pos].trim();

			if (DEBUG_LOGGING_BYTECODE)
			{
				logger.debug("Line: '{}'", line);
			}

			BytecodeSection nextSection = getNextSection(section, line);

			if (nextSection != null)
			{
				sectionFinished(fqClassName, section, msp, builder, memberBytecode, classBytecode);

				section = changeSection(nextSection);
				pos++;

				if (pos < bytecodeLines.length)
				{
					line = bytecodeLines[pos].trim();
				}
			}

			if (DEBUG_LOGGING_BYTECODE)
			{
				logger.debug("{} Line: {}", section, line);
			}

			switch (section)
			{
			case NONE:
				if (couldBeMemberSignature(line))
				{
					msp = MemberSignatureParts.fromBytecodeSignature(fqClassName, line);

					msp.setClassBC(classBytecode);

					if (DEBUG_LOGGING_BYTECODE)
					{
						logger.debug("New signature: {}", msp);
					}

					memberBytecode = new MemberBytecode(classBytecode, msp);

					if (DEBUG_LOGGING_BYTECODE)
					{
						logger.debug("Initialised new MemberBytecode");
					}
				}
				else if (line.startsWith(S_BYTECODE_SIGNATURE))
				{
					buildClassGenerics(line, classBytecode);
				}
				else if (line.startsWith(S_BYTECODE_MINOR_VERSION))
				{
					int minorVersion = getVersionPart(line);

					if (minorVersion != -1)
					{
						classBytecode.setMinorVersion(minorVersion);
					}
				}
				else if (line.startsWith(S_BYTECODE_MAJOR_VERSION))
				{
					int majorVersion = getVersionPart(line);

					if (majorVersion != -1)
					{
						classBytecode.setMajorVersion(majorVersion);
					}
				}
				else if (line.startsWith(S_BYTECODE_CLASSFILE))
				{
					String modules = "/modules/";

					int startIndex = line.indexOf(modules);

					if (startIndex != -1)
					{
						startIndex += modules.length();

						int endIndex = line.indexOf('/', startIndex);

						if (endIndex != -1)
						{
							String moduleName = line.substring(startIndex, endIndex);

							classBytecode.setModuleName(moduleName);
						}
					}
					else
					{
						String jrt = "jrt:/";

						 startIndex = line.indexOf(jrt);

						if (startIndex != -1)
						{
							startIndex += jrt.length();

							int endIndex = line.indexOf('/', startIndex);

							if (endIndex != -1)
							{
								String moduleName = line.substring(startIndex, endIndex);

								classBytecode.setModuleName(moduleName);
							}
						}
					}
				}
				else if (line.startsWith(S_BYTECODE_SOURCE_FILE))
				{
					String sourceFilename = getSourceFile(line);

					if (sourceFilename != null)
					{
						classBytecode.setSourceFile(sourceFilename);

						if (cacheBytecode)
						{
							SourceMapper.addSourceClassMapping(classBytecode);
						}
					}
					else
					{
						logger.warn("Couldn't parse source file from line {}", line);
					}
				}
				break;
			case INNERCLASSES:
				InnerClassRelationship icr = InnerClassRelationship.parse(line);

				if (icr != null)
				{
					if (fqClassName.equals(icr.getParentClass()))
					{
						classBytecode.addInnerClassName(icr.getChildClass());
					}
				}
				else
				{
					section = changeSection(BytecodeSection.NONE);
					pos--;
				}
				break;
			case CODE:
				section = performCODE(fqClassName, classBytecode, builder, section, msp, memberBytecode, line);
				break;
			case CONSTANT_POOL:
				section = performConstantPool(fqClassName, classBytecode, builder, section, msp, memberBytecode, line);
				break;
			case LINETABLE:
				section = performLINETABLE(fqClassName, classBytecode, builder, section, msp, memberBytecode, line);
				break;
			case RUNTIMEVISIBLEANNOTATIONS:
				if (!isRunTimeVisibleAnnotation(line))
				{
					section = changeSection(BytecodeSection.NONE);
					pos--;
				}
				break;
			case LOCALVARIABLETABLE:
				if (!isLocalVariableLine(line))
				{
					section = changeSection(BytecodeSection.NONE);
					pos--;
				}
				break;
			case STACKMAPTABLE:
				if (!isStackMapTable(line))
				{
					section = changeSection(BytecodeSection.NONE);
					pos--;
				}
				break;
			case EXCEPTIONTABLE:
				section = performEXCEPTIONTABLE(fqClassName, classBytecode, builder, section, msp, memberBytecode, line);
				break;
			}

			pos++;
		}

		return classBytecode;
	}

	public static void buildClassGenerics(String line, ClassBC classBytecode)
	{
		StringBuilder keyBuilder = new StringBuilder();
		StringBuilder valBuilder = new StringBuilder();

		boolean inKey = false;
		boolean inVal = false;

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

			if (c == C_OPEN_ANGLE)
			{
				inKey = true;
				inVal = false;
			}
			else if (c == C_COLON)
			{
				if (inKey && !inVal)
				{
					inKey = false;
					inVal = true;
				}
			}
			else if (c == C_SEMICOLON)
			{
				if (!inKey && inVal)
				{
					String key = keyBuilder.toString();
					String val = valBuilder.toString();

					if (val.length() > 0)
					{
						val = val.substring(1); // string leading 'L'
						val = val.replace(S_SLASH, S_DOT);
					}

					classBytecode.addGenericsMapping(key, val);

					keyBuilder.setLength(0);
					valBuilder.setLength(0);

					inKey = true;
					inVal = false;
				}
			}
			else if (inKey)
			{
				keyBuilder.append(c);
			}
			else if (inVal)
			{
				valBuilder.append(c);
			}
		}

		if (!inKey && inVal)
		{
			String key = keyBuilder.toString();
			String val = valBuilder.toString();

			if (val.length() > 0)
			{
				val = val.substring(1); // string leading 'L'
				val = val.replace(S_SLASH, S_DOT);
			}

			classBytecode.addGenericsMapping(key, val);
			keyBuilder.setLength(0);
			valBuilder.setLength(0);

			inKey = false;
			inVal = false;
		}
	}

	private static BytecodeSection performLINETABLE(String fqClassName, ClassBC classBytecode, StringBuilder builder,
			BytecodeSection section, MemberSignatureParts msp, MemberBytecode memberBytecode, String line)
	{
		if (line.startsWith("line "))
		{
			builder.append(line).append(C_NEWLINE);
		}
		else
		{
			sectionFinished(fqClassName, BytecodeSection.LINETABLE, msp, builder, memberBytecode, classBytecode);

			section = changeSection(BytecodeSection.NONE);
		}

		return section;
	}

	private static BytecodeSection performEXCEPTIONTABLE(String fqClassName, ClassBC classBytecode, StringBuilder builder,
			BytecodeSection section, MemberSignatureParts msp, MemberBytecode memberBytecode, String line)
	{
		if (line.contains(" Class "))
		{
			builder.append(line).append(C_NEWLINE);
		}
		else if (line.replace(S_SPACE, S_EMPTY).equals("fromtotargettype"))
		{
		}
		else
		{
			sectionFinished(fqClassName, BytecodeSection.EXCEPTIONTABLE, msp, builder, memberBytecode, classBytecode);

			section = changeSection(BytecodeSection.NONE);
		}

		return section;
	}

	private static BytecodeSection performConstantPool(String fqClassName, ClassBC classBytecode, StringBuilder builder,
			BytecodeSection section, MemberSignatureParts msp, MemberBytecode memberBytecode, String line)
	{
		if (!line.startsWith(S_HASH))
		{
			sectionFinished(fqClassName, BytecodeSection.CONSTANT_POOL, msp, builder, memberBytecode, classBytecode);

			section = changeSection(BytecodeSection.NONE);
		}
		return section;
	}

	private static BytecodeSection performCODE(String fqClassName, ClassBC classBytecode, StringBuilder builder,
			BytecodeSection section, MemberSignatureParts msp, MemberBytecode memberBytecode, final String line)
	{
		int firstColonIndex = line.indexOf(C_COLON);

		if (firstColonIndex != -1)
		{
			String beforeColon = line.substring(0, firstColonIndex);

			try
			{
				// line number ?
				Integer.parseInt(beforeColon);

				builder.append(line).append(C_NEWLINE);
			}
			catch (NumberFormatException nfe)
			{
				if (S_DEFAULT.equals(beforeColon))
				{
					// possibly inside a tableswitch or lookupswitch
					builder.append(line).append(C_NEWLINE);
				}
				else
				{
					sectionFinished(fqClassName, BytecodeSection.CODE, msp, builder, memberBytecode, classBytecode);
					section = changeSection(BytecodeSection.NONE);
				}
			}
		}
		else if (S_CLOSE_BRACE.equals(line.trim()))
		{
			// end of a tableswitch or lookupswitch
			builder.append(line).append(C_NEWLINE);
		}
		return section;
	}

	private static boolean isRunTimeVisibleAnnotation(final String line)
	{
		return line.contains(": #");
	}

	private static boolean isLocalVariableLine(final String line)
	{
		return line.startsWith("Start") || (line.length() > 0 && Character.isDigit(line.charAt(0)));
	}

	private static boolean isStackMapTable(final String line)
	{
		String trimmedLine = line.trim();
		return trimmedLine.startsWith("frame_type") || trimmedLine.startsWith("offset_delta") || trimmedLine.startsWith("locals")
				|| trimmedLine.startsWith("stack");
	}

	private static boolean couldBeMemberSignature(String line)
	{
		return line.endsWith(");") || line.contains(" throws ") && line.endsWith(S_SEMICOLON) || line.startsWith(
				S_BYTECODE_STATIC_INITIALISER_SIGNATURE);
	}

	private static void sectionFinished(String fqClassName, BytecodeSection lastSection, MemberSignatureParts msp,
			StringBuilder builder, MemberBytecode memberBytecode, ClassBC classBytecode)
	{
		if (DEBUG_LOGGING_BYTECODE)
		{
			logger.debug("sectionFinished: {}", lastSection);
		}

		if (lastSection == BytecodeSection.CODE)
		{
			List<BytecodeInstruction> instructions = parseInstructions(builder.toString());

			if (memberBytecode != null)
			{
				memberBytecode.setInstructions(instructions);

				classBytecode.addMemberBytecode(memberBytecode);

				if (DEBUG_LOGGING_BYTECODE)
				{
					logger.debug("stored bytecode for:\n{}", msp);
				}
			}
			else
			{
				logger.error("No member for these instructions");

				for (BytecodeInstruction instr : instructions)
				{
					logger.error("{}", instr);
				}
			}
		}
		else if (lastSection == BytecodeSection.LINETABLE)
		{
			storeLineNumberTable(fqClassName, memberBytecode, builder.toString(), msp);

			if (DEBUG_LOGGING_BYTECODE)
			{
				logger.debug("stored line number table for : {}", msp);
			}
		}
		else if (lastSection == BytecodeSection.EXCEPTIONTABLE)
		{
			storeExceptionTable(fqClassName, memberBytecode, builder.toString(), msp);

			if (DEBUG_LOGGING_BYTECODE)
			{
				logger.debug("stored exception table for : {}", msp);
			}
		}

		builder.delete(0, builder.length());
	}

	private static BytecodeSection changeSection(BytecodeSection nextSection)
	{
		if (DEBUG_LOGGING_BYTECODE)
		{
			logger.debug("Changing section to: {}", nextSection);
		}

		return nextSection;
	}
	
	private static BytecodeSection getNextSection(BytecodeSection currentSection, final String line)
	{
		BytecodeSection nextSection = null;

		if (line != null)
		{
			if (line.isEmpty())
			{
				nextSection = BytecodeSection.NONE;
			}
			else if ( currentSection != BytecodeSection.CODE &&  "}".equals(line.trim()))
			{
				nextSection = BytecodeSection.NONE;
			}

			for (Map.Entry<String, BytecodeSection> entry : sectionLabelMap.entrySet())
			{
				if (line.trim().startsWith(entry.getKey()))
				{
					nextSection = entry.getValue();
					break;
				}
			}
		}

		return nextSection;
	}

	private static int getVersionPart(final String line)
	{
		int version = -1;

		int colonPos = line.indexOf(C_COLON);

		if (colonPos != -1 && colonPos != line.length() - 1)
		{
			String versionPart = line.substring(colonPos + 1).trim();

			try
			{
				version = Integer.parseInt(versionPart);
			}
			catch (NumberFormatException nfe)
			{
			}
		}

		return version;
	}

	private static String getSourceFile(final String line)
	{
		String result = null;

		int colonPos = line.indexOf(C_COLON);

		if (colonPos != -1 && colonPos != line.length() - 1)
		{
			result = line.substring(colonPos + 1);

			result = result.replace(S_DOUBLE_QUOTE, S_EMPTY).trim();
		}

		return result;
	}

	public static List<BytecodeInstruction> parseInstructions(final String bytecode)
	{
		List<BytecodeInstruction> bytecodeInstructions = new ArrayList<>();

		if (DEBUG_LOGGING_BYTECODE)
		{
			logger.debug("Raw bytecode: '{}'", bytecode);
		}

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

		boolean inSwitch = false;
		BCParamSwitch table = new BCParamSwitch();
		BytecodeInstruction instruction = null;

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

			if (DEBUG_LOGGING_BYTECODE)
			{
				logger.debug("parsing bytecode line: '{}' inSwitch: {}", line, inSwitch);
			}

			if (inSwitch)
			{
				if (S_CLOSE_BRACE.equals(line))
				{
					instruction.addParameter(table);

					bytecodeInstructions.add(instruction);
					inSwitch = false;

					if (DEBUG_LOGGING_BYTECODE)
					{
						logger.debug("finished switch");
					}
				}
				else
				{
					String[] parts = line.split(S_COLON);

					if (parts.length == 2)
					{
						table.put(parts[0].trim(), parts[1].trim());
					}
					else
					{
						logger.error("Unexpected tableswitch entry: " + line);
					}
				}
			}
			else
			{
				try
				{
					Matcher matcher = PATTERN_BYTECODE_INSTRUCTION.matcher(line);

					if (matcher.find())
					{
						instruction = new BytecodeInstruction();

						String offset = matcher.group(1);
						String mnemonic = matcher.group(2);
						String paramString = matcher.group(3);
						String comment = matcher.group(4);

						if (mnemonic.endsWith("_w") && !Opcode.GOTO_W.equals(mnemonic) && !Opcode.JSR_W.equals(mnemonic)
								&& !Opcode.LDC_W.equals(mnemonic) && !Opcode.LDC2_W.equals(mnemonic))
						{
							mnemonic = mnemonic.substring(0, mnemonic.length() - "_w".length());
						}

						instruction.setOffset(Integer.parseInt(offset));
						instruction.setOpcode(Opcode.getByMnemonic(mnemonic));

						if (comment != null && comment.trim().length() > 0)
						{
							instruction.setComment(comment.trim());
						}

						if (instruction.getOpcode() == Opcode.TABLESWITCH || instruction.getOpcode() == Opcode.LOOKUPSWITCH)
						{
							if (DEBUG_LOGGING_BYTECODE)
							{
								logger.debug("Found a table or lookup switch");
							}

							inSwitch = true;
						}
						else
						{
							if (paramString != null && paramString.trim().length() > 0)
							{
								processParameters(paramString.trim(), instruction);
							}

							bytecodeInstructions.add(instruction);
						}
					}
					else
					{
						logger.error("could not parse bytecode: '" + line + "'");
					}
				}
				catch (Exception e)
				{
					logger.error("Error parsing bytecode line: '" + line + "'", e);
				}
			}
		}

		return bytecodeInstructions;
	}

	private static void storeLineNumberTable(String fqClassName, MemberBytecode memberBytecode, String tableLines,
			MemberSignatureParts msp)
	{
		String[] lines = tableLines.split(S_NEWLINE);

		for (String line : lines)
		{
			// strip off 'line '
			line = line.trim().substring(5);

			String[] parts = line.split(S_COLON);

			if (parts.length == 2)
			{
				String source = parts[0].trim();
				String offset = parts[1].trim();

				try
				{
					LineTableEntry entry = new LineTableEntry(Integer.parseInt(source), Integer.parseInt(offset));
					memberBytecode.addLineTableEntry(entry);
				}
				catch (NumberFormatException nfe)
				{
					logger.error("Could not parse LineTableEntry {}", line, nfe);
				}
			}
			else
			{
				logger.error("Could not split LineTableEntry line: {}", line);
			}
		}
	}

	private static void storeExceptionTable(String fqClassName, MemberBytecode memberBytecode, String exceptionLines,
			MemberSignatureParts msp)
	{
		String[] lines = exceptionLines.split(S_NEWLINE);

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

			if (line.length() > 0)
			{
				String[] parts = line.split("\\s+");

				if (parts.length == 5)
				{
					try
					{
						int from = Integer.parseInt(parts[0].trim());
						int to = Integer.parseInt(parts[1].trim());
						int target = Integer.parseInt(parts[2].trim());
						String type = parts[4].trim();

						ExceptionTableEntry entry = new ExceptionTableEntry(from, to, target, type);

						memberBytecode.addExceptionTableEntry(entry);
					}
					catch (NumberFormatException nfe)
					{
						logger.error("Could not parse ExceptionTableEntry {}", line, nfe);
					}
				}
				else
				{
					logger.error("Could not split ExceptionTableEntry line: '{}' got parts {}", line, parts.length);
				}
			}
		}
	}

	private static void processParameters(String paramString, BytecodeInstruction instruction)
	{
		String[] parts = paramString.split(S_COMMA);

		for (String part : parts)
		{
			IBytecodeParam parameter;

			part = part.trim();

			if (part.charAt(0) == C_HASH)
			{
				parameter = new BCParamConstant(part);
			}
			else
			{
				try
				{
					int value = Integer.parseInt(part);
					parameter = new BCParamNumeric(value);
				}
				catch (NumberFormatException nfe)
				{
					parameter = new BCParamString(part);
				}
			}

			instruction.addParameter(parameter);
		}
	}
}
