#symbols.py
#Create a list of the symbols in an xc8 *.sym file
#Also allows looking up an address to find the corresponding symbol and
#saving and merging symbol tables

import sys
import json
import bisect
import os.path

class XC8SymbolTable:
	def __init__(self, path):
		ext = os.path.splitext(path)[1]

		if ext == '.sym':
			self._parse_symtab(path)
			self._calc_sizes()
		elif ext == '.stb':
			with open(path, "r") as f:
				keys = json.load(f)
				self.known_functions = keys['functions']
				self.global_vars = keys['globals']
				self.local_vars = keys['locals']
		else:
			raise ValueError("Invalid symbol file with extension != [sym|stb]: %s" % path)
		
		self._build_lookup()

	def _parse_symtab(self, path):
		print path
		lines = [line.strip() for line in open(path)]

		self.symtab = {}
		self.sects = {}
		self.known_functions = []
		self.local_vars = []
		self.global_vars = []

		for line in lines:
			#The data after the segments section does not contain any symbols.
			if line == "%segments":	
				break

			fields = line.split(' ')

			if len(fields) != 5:
				raise ValueError("File did not have a valid format, too many fields in line:\n%s\n" % line)

			name = fields[0]
			addr = int(fields[1], 16)
			sect = fields[3]

			if name.startswith('__end_of'):
				self.known_functions.append((name[8:], addr))
				continue
			elif name.startswith('__size_of'):
				continue

			#Process ram symbols as either local or global variables
			if sect != 'CODE':
				self._process_variable(name, addr)

			self.symtab[name] = (addr, sect)

			#Keep track of which symbols are in which sections
			if sect not in self.sects:
				self.sects[sect] = [name]
			else:
				self.sects[sect].append(name)

	def _process_variable(self, name, addr):
		if '@' in name:
			self.local_vars.append((name, addr))
		else:
			self.global_vars.append((name, addr))

	def merge(self, path):
		"""
		Merge another symbol table into this one
		"""

		other = XC8SymbolTable(path)

		self.known_functions += other.known_functions
		self._build_lookup()

	def _calc_sizes(self):
		"""
		Calculate the sizes of all known_functions: functions with a 
		symbol defining the end of the function, i.e. __end_of_foo
		
		Converts the known_functions list in 3-tuples with (name, starting_address, size)
		"""

		self.known_functions = map(lambda x: (x[0], self.symtab[x[0]][0], x[1] - self.symtab[x[0]][0]), self.known_functions)
		self.total_size = reduce(lambda x,y: x+y, map(lambda x: x[2], self.known_functions))

	def _build_lookup(self):
		self.known_functions = sorted(self.known_functions, key=lambda x:x[1])
		self.lookup = map(lambda x: x[1], self.known_functions)

	def map_address(self, addr):
		"""
		Given an address, find the function and offset within that function that it belongs to
		"""

		i = bisect.bisect_right(self.lookup, addr)
		i -= 1

		if i < 0:
			return None

		offset = addr - self.known_functions[i][1]

		if offset < self.known_functions[i][2]:
			return (self.known_functions[i][0], offset)

		return None

	def _check_c_name(self, var):
		if '$' in var or '?' in var:
			return False

		return True

	def _gen_variable_def(self, var):
		if not self._check_c_name(var[0]):
			return None

		line = '#define %s_variable 0x%x\n' % (var[0], var[1])
		return line

	def _gen_define(self, func):
		if not self._check_c_name(func[0]):
			return None

		line = '#define %s_address 0x%x\n' % (func[0], func[1])
		return line

	def _gen_c_call(self, func):
		if not self._check_c_name(func[0]):
			return None

		#Use fcall in case call is not in the first page of rom
		line = '#define c_call%s() asm("fcall 0x%x")\n' % (func[0], func[1])
		return line 

	def _gen_asm_call(self, func):
		if not self._check_c_name(func[0]):
			return None

		line = '#define asm_call%s() fcall 0x%x\n' % (func[0], func[1])
		return line 

	def generate_h_file(self, path):
		defs = map(lambda x: self._gen_define(x), self.known_functions)
		c_calls = map(lambda x: self._gen_c_call(x), self.known_functions)
		asm_calls = map(lambda x: self._gen_asm_call(x), self.known_functions)
		global_vars = map(lambda x: self._gen_variable_def(x), self.global_vars)

		defs = filter(lambda x: x is not None, defs)
		c_calls = filter(lambda x: x is not None, c_calls)
		asm_calls = filter(lambda x: x is not None, asm_calls)
		global_vars = filter(lambda x: x is not None, global_vars)

		with open(path, "w") as f:
			f.write('//Autogenerated Symbol Header File (do not modify)\n')
			f.write('//Known Functions (addresses and call macros)\n')
			f.writelines(defs)
			f.writelines(c_calls)
			f.writelines(asm_calls)
			f.write('\n//Global Variables\n')
			f.writelines(global_vars)

	def generate_stb_file(self, path):
		with open(path, "w") as f:
			json.dump({'functions': self.known_functions, 'globals': self.global_vars, 'locals': self.local_vars}, f)
