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

import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_SPACE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_AT;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_CLOSE_PARENTHESES;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_DOLLAR;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_NEWLINE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_OPEN_PARENTHESES;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_SPACE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_HEX_PREFIX;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_HEX_POSTFIX;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;

import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;

import org.adoptopenjdk.jitwatch.model.assembly.AssemblyBlock;
import org.adoptopenjdk.jitwatch.model.assembly.AssemblyInstruction;
import org.adoptopenjdk.jitwatch.model.assembly.AssemblyMethod;
import org.adoptopenjdk.jitwatch.model.assembly.AssemblyReference;
import org.adoptopenjdk.jitwatch.model.assembly.AssemblyUtil;
import org.adoptopenjdk.jitwatch.model.assembly.IAssemblyParser;
import org.adoptopenjdk.jitwatch.model.bytecode.BytecodeInstruction;
import org.adoptopenjdk.jitwatch.ui.main.IStageAccessProxy;
import org.adoptopenjdk.jitwatch.ui.triview.ILineListener;
import org.adoptopenjdk.jitwatch.ui.triview.ILineListener.LineType;
import org.adoptopenjdk.jitwatch.ui.triview.Viewer;
import org.adoptopenjdk.jitwatch.util.StringUtil;

// https://github.com/yasm/yasm/wiki/GasSyntax
// http://x86asm.net/articles/what-i-dislike-about-gas/
// http://vaskoz.wordpress.com/2013/07/14/hotspot-disassembler/
// https://sourceware.org/binutils/docs/as/i386_002dRegs.html#i386_002dRegs

public class ViewerAssembly extends Viewer
{
	private DecimalFormat formatThousandsUnderscore;

	private IAssemblyParser parser; // TODO choose parser

	public ViewerAssembly(IStageAccessProxy stageAccessProxy, ILineListener lineListener, LineType lineType)
	{
		super(stageAccessProxy, lineListener, lineType, true);

		formatThousandsUnderscore = (DecimalFormat) NumberFormat.getInstance();

		DecimalFormatSymbols symbols = formatThousandsUnderscore.getDecimalFormatSymbols();

		symbols.setGroupingSeparator('_');

		formatThousandsUnderscore.setDecimalFormatSymbols(symbols);
	}

	public void setAssemblyMethod(AssemblyMethod asmMethod, boolean showLocalLabels)
	{
		parser = AssemblyUtil.getParserForArchitecture(asmMethod.getArchitecture());

		lastScrollIndex = -1;

		List<Label> labels = new ArrayList<>();

		int annoWidth = asmMethod.getMaxAnnotationWidth();

		String annoPad = StringUtil.repeat(C_SPACE, annoWidth);

		String header = asmMethod.getHeader();

		if (header != null)
		{
			String[] headerLines = header.split(S_NEWLINE);

			for (String headerLine : headerLines)
			{
				labels.add(createLabel(annoPad + headerLine));
			}
		}

		for (AssemblyBlock block : asmMethod.getBlocks())
		{
			String title = block.getTitle();

			if (title != null)
			{
				labels.add(createLabel(annoPad + title));
			}

			for (final AssemblyInstruction instr : block.getInstructions())
			{
				List<String> commentLines = instr.getCommentLines();

				if (commentLines.size() == 0)
				{
					Label lblLine = createLabel(instr, annoWidth, 0, showLocalLabels);
					lblLine.setTooltip(new Tooltip(getToolTip(instr)));
					labels.add(lblLine);
				}
				else
				{
					for (int i = 0; i < commentLines.size(); i++)
					{
						Label lblLine = createLabel(instr, annoWidth, i, showLocalLabels);
						lblLine.setTooltip(new Tooltip(getToolTip(instr)));
						labels.add(lblLine);
					}
				}
			}
		}

		setContent(labels);
	}

	private String getToolTip(AssemblyInstruction instruction)
	{
		StringBuilder builder = new StringBuilder();

		String mnemonic = instruction.getMnemonic();

		String ref = AssemblyReference.lookupMnemonic(mnemonic);

		if (ref == null)
		{
			ref = "Unknown instruction. Assembly reference loaded?";
		}

		builder.append(ref).append(S_NEWLINE);

		List<String> operands = instruction.getOperands();

		// AT&T = source, dest
		// Intel = dest, source

		int pos = 1;

		for (String operand : operands)
		{
			builder.append("operand ").append(pos).append(": ");

			decodeOperand(mnemonic, operand, builder);

			builder.append(S_NEWLINE);

			pos++;
		}

		return builder.toString();
	}

	private void decodeOperand(String mnemonic, String operand, StringBuilder builder)
	{
		if (parser.isRegister(mnemonic, operand))
		{
			builder.append(decodeRegister(operand));
		}
		else if (parser.isConstant(mnemonic, operand))
		{
			builder.append(getConstantLabel(operand));
		}
		else if (parser.isAddress(mnemonic, operand))
		{
			builder.append("Address ");
			builder.append(operand);
		}
	}

	private String getConstantLabel(String operand)
	{
		StringBuilder builder = new StringBuilder();

		builder.append("Constant ");
		builder.append(operand);

		try
		{
			if (operand.startsWith(S_DOLLAR))
			{
				operand = operand.substring(1);
			}

			if (operand.endsWith(S_HEX_POSTFIX))
			{
				operand = S_HEX_PREFIX + operand.substring(0, operand.length() - 1);
			}

			long decimal = Long.decode(operand);

			builder.append(S_SPACE).append(S_OPEN_PARENTHESES).append("Decimal: ").append(formatThousandsUnderscore.format(decimal))
					.append(S_CLOSE_PARENTHESES);
		}
		catch (NumberFormatException nfe)
		{
			// e.g. $0xffffffffffffffff - ignore it
		}

		return builder.toString();
	}

	private String decodeRegister(String input)
	{
		StringBuilder builder = new StringBuilder();

		// https://sourceware.org/binutils/docs/as/i386_002dRegs.html#i386_002dRegs
		// http://www.x86-64.org/documentation/assembly.html

		String regName = parser.extractRegisterName(input);

		if (regName.startsWith("e"))
		{
			builder.append("32-bit register ").append(regName);
		}
		else if (regName.startsWith("r"))
		{
			builder.append("64-bit register ").append(regName);
		}
		else if (regName.startsWith("db"))
		{
			builder.append("debug register ").append(regName);
		}
		else if (regName.startsWith("cr"))
		{
			builder.append("processor control register ").append(regName);
		}
		else if (regName.startsWith("tr"))
		{
			builder.append("test register ").append(regName);
		}
		else if (regName.startsWith("st"))
		{
			builder.append("floating point register stack ").append(regName);
		}
		else if (regName.startsWith("mm"))
		{
			builder.append("MMX register ").append(regName);
		}
		else if (regName.startsWith("xmm"))
		{
			builder.append("SSE register ").append(regName);
		}
		else if (regName.endsWith("s"))
		{
			builder.append("section register ").append(regName);
		}
		else
		{
			builder.append("Register ").append(regName);
		}

		if (regName.endsWith("ax"))
		{
			builder.append(" (accumulator)");
		}
		else if (regName.endsWith("bp"))
		{
			builder.append(" (frame pointer)");
		}
		else if (regName.endsWith("sp"))
		{
			builder.append(" (stack pointer)");
		}

		if (input.startsWith("*"))
		{
			builder.append(" (indirect)");
		}

		return builder.toString();
	}

	private Label createLabel(String text)
	{
		Label lbl = new Label(text);

		lbl.setStyle(STYLE_UNHIGHLIGHTED);

		return lbl;
	}

	private AssemblyLabel createLabel(AssemblyInstruction instruction, int annoWidth, int line, boolean showLocalLabels)
	{
		AssemblyLabel lbl = new AssemblyLabel(instruction, annoWidth, line, showLocalLabels);

		if (instruction.isSafePoint())
		{
			lbl.setUnhighlightedStyle(STYLE_SAFEPOINT);
		}

		return lbl;
	}

	public String getClassNameFromLabel(Label label)
	{
		String result = null;

		if (label != null)
		{
			String line = label.getText();

			result = StringUtil.getSubstringBetween(line, "; - ", "::");
		}

		return result;
	}

	public String getSourceLineFromLabel(Label label)
	{
		String result = null;

		if (label != null)
		{
			String line = label.getText();

			result = StringUtil.getSubstringBetween(line, "(line ", S_CLOSE_PARENTHESES);
		}

		return result;
	}

	public String getBytecodeOffsetFromLabel(Label label)
	{
		String result = null;

		if (label != null)
		{
			String line = label.getText();

			result = StringUtil.getSubstringBetween(line, S_AT, S_SPACE);
		}

		return result;
	}

	public int getIndexForSourceLine(String memberClassName, int sourceIndex)
	{
		int result = -1;

		int pos = 0;

		for (Node node : vBoxRows.getChildren())
		{
			Label label = (Label) node;

			String className = getClassNameFromLabel(label);

			if (className != null && className.equals(memberClassName))
			{
				String labelSourceLine = getSourceLineFromLabel(label);

				if (labelSourceLine != null && labelSourceLine.equals(Integer.toString(sourceIndex)))
				{
					result = pos;
					break;
				}
			}

			pos++;
		}

		return result;
	}

	public int getIndexForBytecodeOffset(String memberClassName, BytecodeInstruction bcInstruction)
	{
		int result = -1;
		int pos = 0;

		boolean isInvoke = bcInstruction.getOpcode().isInvoke();
		int bytecodeOffset = bcInstruction.getOffset();

		boolean lastAssemblyIsCall = false;

		for (Node node : vBoxRows.getChildren())
		{
			Label label = (Label) node;

			if (label instanceof AssemblyLabel)
			{
				AssemblyInstruction lastAssemblyInstruction = ((AssemblyLabel) label).getInstruction();

				String mnemonic = lastAssemblyInstruction.getMnemonic();

				if (mnemonic != null && mnemonic.toLowerCase().startsWith("call"))
				{
					lastAssemblyIsCall = true;
				}
				else
				{
					lastAssemblyIsCall = false;
				}
			}

			String className = getClassNameFromLabel(label);

			if (className != null && className.equals(memberClassName))
			{
				String labelOffsetLine = getBytecodeOffsetFromLabel(label);

				if (labelOffsetLine != null && labelOffsetLine.equals(Integer.toString(bytecodeOffset)))
				{
					if (isInvoke && lastAssemblyIsCall)
					{
						result = pos;
						break;
					}
					else if (result == -1)
					{
						result = pos;
					}
				}
			}

			pos++;
		}

		return result;
	}
}