# -*- coding: utf-8 -*-
# Quick and Dirty Game Boy Assembler
import re
import sys
import collections

class InputError(RuntimeError): pass
Op = collections.namedtuple('Op', ['string', 'code', 'size', 'assemble'])

def mkrestr(s):
	return re.sub(r'\s+', r'\s*', re.sub(r'([,)(+])', r' \\\1 ', s))

imm8  = r'[0-9a-fA-F][0-9a-fA-F]'
imm16 = imm8 + imm8
labelregexp = re.compile('\w+\s+(\w+)')

def assembleNormal(outdata, outpos, indata, inpos, targets, op):
	pass

def assembleImm8(outdata, outpos, indata, inpos, targets, op):
	m = re.match(mkrestr(op.string).replace('imm8', '('+imm8+')'), indata[inpos])
	outdata[outpos] = int(m.group(1), 0x10)

def assembleImm16(outdata, outpos, indata, inpos, targets, op):
	m = re.match(mkrestr(op.string).replace('imm16', '('+imm16+')'), indata[inpos])
	outdata[outpos  ] = int(m.group(1)[2:4], 0x10)
	outdata[outpos+1] = int(m.group(1)[0:2], 0x10)

def assembleJr(outdata, outpos, indata, inpos, targets, op):
	label = labelregexp.match(indata[inpos]).group(1)
	t = targets.get(label)
	t = t - (outpos + 1) if t != None else int(label, 0x10)
	
	if t < -0x80 or t > 0x7f:
		raise InputError('line ' + str(i) + ': ' + indata[inpos], 'Relative jump target too far away')
	
	outdata[outpos] = t & 0xff

def assembleJp(outdata, outpos, indata, inpos, targets, op):
	label = labelregexp.match(indata[inpos]).group(1)
	t = targets.get(label)
	
	if t == None:
		t = int(label, 0x10)
	elif t >= 0x8000:
		t = 0x4000 | (t & 0x3fff)
	
	outdata[outpos  ] = t & 0xFF
	outdata[outpos+1] = t >> 8

def makeoplist():
	l = []
	
	def addop(opstr, opcode):
		size = (opcode >= 0) + (opcode >= 0x100)
		moperand = re.match(r'(.*imm8)|(jr.* target)|(.*imm16)|(call.* target|jp.* target)', opstr)
		if moperand:
			optype = [(0, assembleNormal), (1, assembleImm8), (1, assembleJr), (2, assembleImm16), (2, assembleJp)][moperand.lastindex]
			l.append(Op(opstr, opcode, size + optype[0], optype[1]))
		else:
			l.append(Op(opstr, opcode, size, assembleNormal))

	addop('nop', 0x00)
	addop('ld bc, imm16', 0x01)
	addop('ld(bc), a', 0x02)
	addop('inc bc', 0x03)
	addop('inc b', 0x04)
	addop('dec b', 0x05)
	addop('ld b, imm8', 0x06)
	addop('ld a, (bc)', 0x0a)
	addop('inc c', 0x0c)
	addop('dec c', 0x0d)
	addop('ld c, imm8', 0x0e)
	addop('rrca', 0x0f)
	addop('stop, imm8', 0x10)
	addop('ld de, imm16', 0x11)
	addop('ld(de), a', 0x12)
	addop('inc d', 0x14)
	addop('dec d', 0x15)
	addop('ld d, imm8', 0x16)
	addop('jr target', 0x18)
	addop('add hl, de', 0x19)
	addop('ld a, (de)', 0x1a)
	addop('inc e', 0x1c)
	addop('dec e', 0x1d)
	addop('jrnz target', 0x20)
	addop('ld hl, imm16', 0x21)
	addop('ld(hl++), a', 0x22)
	addop('inc h', 0x24)
	addop('dec h', 0x25)
	addop('inc l', 0x2c)
	addop('dec l', 0x2d)
	addop('ld sp, imm16', 0x31)
	addop('ld(hl--), a', 0x32)
	addop('ld(hl), imm8', 0x36)
	addop('ld a, (hl--)', 0x3a)
	addop('inc a', 0x3c)
	addop('dec a', 0x3d)
	addop('ld a, imm8', 0x3e)
	addop('ld b, a', 0x47)
	addop('ld d, a', 0x57)
	addop('halt', 0x76)
	addop('ld(hl), a', 0x77)
	addop('ld a, b', 0x78)
	addop('ld a, c', 0x79)
	addop('ld a, d', 0x7a)
	addop('ld a, e', 0x7b)
	addop('ld a, (hl)', 0x7e)
	addop('sub a, b', 0x90)
	addop('sub a, c', 0x91)
	addop('sub a, d', 0x92)
	addop('and a, b', 0xa0)
	addop('and a, c', 0xa1)
	addop('and a, d', 0xa2)
	addop('xor a, a', 0xaf)
	addop('or a, a', 0xb7)
	addop('cmp a, b', 0xb8)
	addop('cmp a, c', 0xb9)
	addop('cmp a, d', 0xba)
	addop('cmp a, e', 0xbb)
	addop('cmp a, h', 0xbc)
	addop('jpnz target', 0xc2)
	addop('jp target', 0xc3)
	addop('ret', 0xc9)
	addop('call target', 0xcd)
	addop('pop de', 0xd1)
	addop('sub a, imm8', 0xd6)
	addop('ldff(imm8), a', 0xe0)
	addop('pop hl', 0xe1)
	addop('ldff(c), a', 0xe2)
	addop('push hl', 0xe5)
	addop('and a, imm8', 0xe6)
	addop('ld(imm16), a', 0xea)
	addop('xor a, imm8', 0xee)
	addop('ldff a, (imm8)', 0xf0)
	addop('ldff a, (c)', 0xf2)
	addop('ld a, (imm16)', 0xfa)
	addop('ei', 0xfb)
	addop('cmp a, imm8', 0xfe)
	addop('srl a', 0xcb3f)
	addop('\w* : ', -1)
	
	return l

def makeopregexp(oplist):
	rxps = ''
	for e in oplist:
		rxps += '(' + mkrestr(e.string) + ')|'
	return re.compile(rxps[0:len(rxps)-1].replace('imm8', imm8).replace('imm16', imm16).replace('target', r'\s\w+'))

oplist = makeoplist()
opregexp = makeopregexp(oplist)

def maptargets(targets, indata, instart, addr):
	for i in xrange(instart, len(indata)):
		match = opregexp.match(indata[i])
		if match == None:
			break
		
		op = oplist[match.lastindex - 1]
		
		if op.size == 0:
			targets[re.match('(\w*)\s*:', indata[i]).group(1)] = addr
			
		addr += op.size
	
	return i

def ascode(outdata, addr, indata, instart, targets):
	for i in xrange(instart, len(indata)):
		match = opregexp.match(indata[i])
		if match == None:
			break
		
		op = oplist[match.lastindex - 1]
		outpos = addr
		
		if op.code >= 0x100:
			outdata[outpos  ] = op.code >> 8
			outdata[outpos+1] = op.code & 0xff
			outpos += 2
		elif op.code >= 0:
			outdata[outpos] = op.code
			outpos += 1
		
		op.assemble(outdata, outpos, indata, i, targets, op)
			
		addr += op.size
	
	return i

def asdata(outdata, addr, indata, instart):
	try:
		for i in xrange(instart, len(indata)):
			ints = [int(x, 0x10) for x in indata[i].split()]
			outdata[addr:addr+len(ints)] = ints
			addr += len(ints)
	except ValueError:
		pass
	
	return i

def putgblogo(outdata):
	outdata[0x104:0x134] = \
		[0xce, 0xed, 0x66, 0x66, 0xcc, 0x0d, 0x00, 0x0b,
		 0x03, 0x73, 0x00, 0x83, 0x00, 0x0c, 0x00, 0x0d,
		 0x00, 0x08, 0x11, 0x1f, 0x88, 0x89, 0x00, 0x0e,
		 0xdc, 0xcc, 0x6e, 0xe6, 0xdd, 0xdd, 0xd9, 0x99,
		 0xbb, 0xbb, 0x67, 0x63, 0x6e, 0x0e, 0xec, 0xcc,
		 0xdd, 0xdc, 0x99, 0x9f, 0xbb, 0xb9, 0x33, 0x3e]

def putheadercsum(outdata):
	outdata[0x14d] = sum([-(x+1) for x in outdata[0x134:0x14d]]) & 0xFF

def putromcsum(outdata):
	csum = sum(outdata)
	outdata[0x14e:0x150] = [csum >> 8 & 0xff, csum & 0xff]

def assembleFile(indata):
	size = 0x8000
	startpos = 0

	for i in xrange(0, len(indata)):
		spl = indata[i].split()
		if len(spl) > 0 and spl[0] == '.size':
			size = int(spl[1], 0x10)
			startpos = i + 1
			break

	targets = {}
	i = startpos
	while i < len(indata):
		spl = [x.strip() for x in indata[i].split('@')]
		i += 1;
		
		if spl[0] == '.code':
			i = maptargets(targets, indata, i, int(spl[1], 0x10))

	outdata = bytearray(size)
	i = startpos
	while i < len(indata):
		if indata[i] != '':
			spl = [x.strip() for x in indata[i].split('@')]
			
			if spl[0] == '.code':
				i = ascode(outdata, int(spl[1], 0x10), indata, i+1, targets)
			elif spl[0] == '.data':
				i = asdata(outdata, int(spl[1], 0x10), indata, i+1)
			else:
				raise RuntimeError('line ' + str(i) + ': ' + indata[i])
		else:
			i += 1


	putgblogo(outdata)
	putheadercsum(outdata)
	putromcsum(outdata)
	
	return outdata

def readDataFromFile(filename):
	infile = open(filename, 'r')
	indata = [line.strip() for line in infile]
	infile.close()
	return indata

def writeDataToFile(filename, data):
	outfile = open(filename, 'wb')
	outfile.write(data)
	outfile.close()

def outFilenameFromInFilename(inname, h143):
	return inname.rsplit('.', 1)[0] + ('.gbc' if (h143 & 0x80) else '.gb')

def main():
	for arg in sys.argv[1:]:
		print 'processing ' + arg
		outdata = assembleFile(readDataFromFile(arg))
		writeDataToFile(outFilenameFromInFilename(arg, outdata[0x143]), outdata)
	
	print "\nassembled " + str(len(sys.argv) - 1) + (' files' if len(sys.argv) != 2 else ' file')

if __name__ == "__main__":
	main()
