# Copyright (c) 2019-2024 Vector 35 Inc
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

from binaryninja.function import DisassemblyTextRenderer, DisassemblyTextLine
from binaryninja.lineardisassembly import LinearDisassemblyLine
from binaryninja.enums import LinearDisassemblyLineType, DisassemblyOption
from binaryninjaui import TokenizedTextView, TokenizedTextViewHistoryEntry, ViewType


class LinearMLILView(TokenizedTextView):
	def __init__(self, parent, data):
		super(LinearMLILView, self).__init__(parent, data)
		self.data = data
		self.function = data.entry_function
		if self.function is not None:
			self.setFunction(self.function)
			self.updateLines()

	def generateLines(self):
		if self.function is None:
			return []

		il = self.function.mlil

		# Set up IL display options
		renderer = DisassemblyTextRenderer(il)
		renderer.settings.set_option(DisassemblyOption.ShowAddress)
		renderer.settings.set_option(DisassemblyOption.ShowVariableTypesWhenAssigned)

		# Sort basic blocks by IL instruction index
		blocks = il.basic_blocks
		list(blocks).sort(key=lambda block: block.start)

		# Function header
		result = []
		result.append(
		  LinearDisassemblyLine(
		    LinearDisassemblyLineType.FunctionHeaderStartLineType, self.function, None,
		    DisassemblyTextLine([], self.function.start)
		  )
		)
		result.append(
		  LinearDisassemblyLine(
		    LinearDisassemblyLineType.FunctionHeaderLineType, self.function, None,
		    DisassemblyTextLine(self.function.type_tokens, self.function.start)
		  )
		)
		result.append(
		  LinearDisassemblyLine(
		    LinearDisassemblyLineType.FunctionHeaderEndLineType, self.function, None,
		    DisassemblyTextLine([], self.function.start)
		  )
		)

		# Display IL instructions in order
		lastAddr = self.function.start
		lastBlock = None
		lineIndex = 0
		for block in il:
			if lastBlock is not None:
				# Blank line between basic blocks
				result.append(
				  LinearDisassemblyLine(
				    LinearDisassemblyLineType.CodeDisassemblyLineType, self.function, block, DisassemblyTextLine([], lastAddr)
				  )
				)
			for i in block:
				lines = renderer.get_disassembly_text(i.instr_index)
				lastAddr = i.address
				lineIndex = 0
				for line in lines:
					result.append(
					  LinearDisassemblyLine(LinearDisassemblyLineType.CodeDisassemblyLineType, self.function, block, line[0])
					)
					lineIndex += 1
			lastBlock = block

		result.append(
		  LinearDisassemblyLine(
		    LinearDisassemblyLineType.FunctionEndLineType, self.function, lastBlock, DisassemblyTextLine([], lastAddr)
		  )
		)

		return result

	def updateLines(self):
		self.setUpdatedLines(self.generateLines())

	def navigate(self, addr):
		# Find correct function based on most recent use
		block = self.data.get_recent_basic_block_at(addr)
		if block is None:
			# If function isn't done analyzing yet, it may have a function start but no basic blocks
			func = self.data.get_recent_function_at(addr)
		else:
			func = block.function

		if func is None:
			# No function contains this address, fail navigation in this view
			return False

		self.function = func
		self.setFunction(self.function)
		self.setLines(self.generateLines())
		return True

	def getHistoryEntry(self):
		class LinearMLILHistoryEntry(TokenizedTextViewHistoryEntry):
			def __init__(self, function_start):
				TokenizedTextViewHistoryEntry.__init__(self)
				self.function_start = function_start

			def serialize(self):
				v = TokenizedTextViewHistoryEntry.serialize(self)
				v["function_start"] = self.function_start
				return v

			def deserialize(self, v):
				if not TokenizedTextViewHistoryEntry.deserialize(self, v):
					return False
				self.function_start = v["function_start"]
				return True

		result = LinearMLILHistoryEntry(self.function.start)
		self.populateDefaultHistoryEntry(result)
		return result

	def navigateToHistoryEntry(self, entry):
		if hasattr(entry, 'function_start'):
			self.function = self.data.get_functions_at(entry.function_start)[0]
			self.setFunction(self.function)
			self.updateLines()
		super(LinearMLILView, self).navigateToHistoryEntry(entry)


# View type for the new view
class LinearMLILViewType(ViewType):
	def __init__(self):
		super(LinearMLILViewType, self).__init__("Linear MLIL", "Linear MLIL")

	def getPriority(self, data, filename):
		if data.executable:
			# Use low priority so that this view is not picked by default
			return 1
		return 0

	def create(self, data, view_frame):
		return LinearMLILView(view_frame, data)


# Register the view type so that it can be chosen by the user
ViewType.registerViewType(LinearMLILViewType())
