/*
 * 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.jarscan.sequencecount;

import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_NEWLINE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_COMMA;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.adoptopenjdk.jitwatch.jarscan.IJarScanOperation;
import org.adoptopenjdk.jitwatch.model.bytecode.BCParamNumeric;
import org.adoptopenjdk.jitwatch.model.bytecode.BytecodeInstruction;
import org.adoptopenjdk.jitwatch.model.bytecode.MemberBytecode;
import org.adoptopenjdk.jitwatch.model.bytecode.Opcode;

public class SequenceCountOperation implements IJarScanOperation
{
	protected Map<InstructionSequence, Integer> chainCountMap = new TreeMap<>();

	private List<Opcode> chain = new LinkedList<>();

	private int maxLength = 0;

	public SequenceCountOperation(int maxLength)
	{
		this.maxLength = maxLength;
	}

	private void storeChain()
	{
		InstructionSequence sequence = new InstructionSequence(chain);

		Integer count = chainCountMap.get(sequence);

		if (count == null)
		{
			chainCountMap.put(sequence, 1);
		}
		else
		{
			chainCountMap.put(sequence, count + 1);
		}
	}

	public Map<InstructionSequence, Integer> getSequenceScores()
	{
		return chainCountMap;
	}

	public List<Map.Entry<InstructionSequence, Integer>> getSortedData()
	{
		List<Map.Entry<InstructionSequence, Integer>> result = new ArrayList<>(chainCountMap.entrySet());

		Collections.sort(result, new Comparator<Map.Entry<InstructionSequence, Integer>>()
		{
			@Override
			public int compare(Map.Entry<InstructionSequence, Integer> o1, Map.Entry<InstructionSequence, Integer> o2)
			{
				return o2.getValue().compareTo(o1.getValue());
			}
		});

		return result;
	}

	public void reset()
	{
		chain.clear();
	}

	@Override
	public void processInstructions(String className, MemberBytecode memberBytecode)
	{
		reset();

		List<BytecodeInstruction> instructions = memberBytecode.getInstructions();

		for (int i = 0; i < instructions.size(); i++)
		{
			handleChainStartingAtIndex(i, instructions);
		}
	}

	private void handleChainStartingAtIndex(int index, List<BytecodeInstruction> instructions)
	{
		boolean stopChain = false;
		boolean abandonChain = false;

		Set<Integer> visitedBCI = new HashSet<>();
				
		while (chain.size() < maxLength)
		{
			if(index >=instructions.size())
			{
				break;
			}
			BytecodeInstruction instruction = instructions.get(index);
					
			int instrBCI = instruction.getOffset();

			visitedBCI.add(instrBCI);

			Opcode opcode = instruction.getOpcode();

			// =======================
			// The Rules
			// =======================

			// *RETURN ends a chain. Chain is discarded if not required length
			// INVOKE* drops through to next bytecode
			// GOTO* is followed
			// IF*, TABLESWITCH, and LOOKUPSWITCH - drop through
			// JSR, JSR_W, RET are not followed - discard the chain
			// ATHROW ends a chain
			// loops are detected and end the parsing

			switch (opcode)
			{
			case IRETURN:
			case LRETURN:
			case FRETURN:
			case DRETURN:
			case ARETURN:
			case RETURN:
				stopChain = true;
				break;
				
			case ATHROW:
				stopChain = true;
				break;

			case JSR:
			case JSR_W:
			case RET:
				abandonChain = true;
				break;

			case GOTO:
			case GOTO_W:
				int gotoBCI = ((BCParamNumeric) instruction.getParameters().get(0)).getValue();

				if (!visitedBCI.contains(gotoBCI))
				{
					index = getIndexForBCI(instructions, gotoBCI);
				}
				break;

			default:
				index++;
				break;
			}

			chain.add(opcode);

			if (stopChain)
			{
				if (chain.size() == maxLength)
				{
					storeChain();
				}

				reset();
				break;
			}
			else if (abandonChain)
			{
				reset();
				break;
			}
			else if (chain.size() == maxLength)
			{
				storeChain();
				reset();
				break;
			}
		}
	}

	private int getIndexForBCI(List<BytecodeInstruction> instructions, int bci)
	{
		int index = -1;

		for (int i = 0; i < instructions.size(); i++)
		{
			BytecodeInstruction instruction = instructions.get(i);

			if (instruction.getOffset() == bci)
			{
				index = i;
				break;
			}
		}

		return index;
	}

	@Override
	public String getReport()
	{
		StringBuilder builder = new StringBuilder();

		for (Map.Entry<InstructionSequence, Integer> entry : getSortedData())
		{
			builder.append(entry.getKey().toString()).append(S_COMMA).append(entry.getValue()).append(S_NEWLINE);
		}

		return builder.toString();
	}
}
