# Python 2.7 Code; Jonathan Frech, 24th, 25th of August 2017

"""
	=============
	 BRAINFUCK X 
	=============
	
	Python 2.7 Brainfuck X interpreter; written by Jonathan Frech.
	Brainfuck X is a Brainfuck extension that adds three new operators ('^', 'v', '*') and 24 bit color support. It is also called Braindraw.
	Input files have the .bfx extension.
	
	The source code is one-dimensional, read from left to right.
	Looping works like in Brainfuck, using '[' and ']':
		if the code pointer reaches an open bracket, it is ignored if the current cell is non-zero and jumped to the matching closing bracket if it is zero
		if the code pointer reaches a closed bracket, it is ignored if the current cell is zero and jumped to the (prior) matching open bracket if it is non-zero
	The used tape is three-dimensional, two spacial and one color dimension. Each cell's value is limited to 8 bits (0-255). Values outside the value bound are modulo-wrapped.
	The tape's size is only restricted in the color dimension (there are three color planes).
	Moving around the tape is done via moving in the four cardinal directions using '^', '>', 'v' (only lowercase!) and '<'
	Movement through the three color planes depends on the '--colorletters' flag. (1)
		Using color letters, one can jump to a color plane using its one-letter acronym ('r', 'g' and 'b')
		Not using color letters, one can move forwards through the color planes using '*' (starting at 'r', continuing with 'g' and 'b', then wrapping around).
	Input and output works like in the original Brainfuck; '.' output the current cell's value interpreted as an ASCII character and ',' takes a character as input and saves it in the current cell.

	(1) The original StackExchange answer used the color letters model. Though, I found it more Brainfucky to simply use a single character.
"""

# import
import argparse, time
	
# main function
def main():
	# start timer
	t0 = time.time()
	
	# ==================
	#  ARGUMENT PARSING 
	# ==================
	
	# using argparse to parse command line arguments
	parser = argparse.ArgumentParser(description = "A Brainfuck dialect. Colorful, three-dimensional.")
	parser.add_argument("source"        ,       metavar = "S", type = str                   , help = "Source code file name"                                                                 )
	parser.add_argument("--input"       , "-i", metavar = "I", type = str                   , help = "String as input character stream. It is warned when the input stream is too short."    )
	parser.add_argument("--simplify"    , "-s", action  = "store_true"                      , help = "Ouput the given source code's simplified version (all unnecessary characters removed).")
	parser.add_argument("--colorstar"   ,       action  = "store_true"                      , help = "Use color star ('*') to move through color planes (the default)."                      )
	parser.add_argument("--colorletters", "-l", action  = "store_true"                      , help = "Use color letters ('rgb') to swap color planes."                                       )
	parser.add_argument("--silent"      ,       action  = "store_true"                      , help = "Silent mode, disable infos and warnings."                                              )
	parser.add_argument("--maxcycles"   , "-m", metavar = "C", type = int  , default = 10**6, help = "Maximum number of cycles."                                                             )
	parser.add_argument("--watch"       , "-w", action  = "store_true"                      , help = "Watch the program's execution."                                                        )
	parser.add_argument("--watchdelay"  ,       metavar = "T", type = float, default = .1   , help = "Time between cycles in seconds while watching."                                        )
	parser.add_argument("--watchskip"   ,       metavar = "N", type = int  , default = 1    , help = "Only the Nth cycle is printed in watch mode."                                          )
	parser.add_argument("--output"      , "-o", metavar = "O", type = str                   , help = "Output tape filememory (saved in the ppm file format)."                                )
	parsed = parser.parse_args()
	
	# colored ANSI escape codes; error, warning, info, output, tape, code
	err, wrn, inf = "\033[38;5;9mError\033[0m: ", "\033[38;5;11mWarning\033[0m: ", "\033[38;5;10mInfo\033[0m: "
	out, tap, cod = "\033[38;5;12mOut\033[0m: " , "\033[38;5;12mTape\033[0m: "   , "\033[38;5;12mCode\033[0m: "
	Wrn, Inf, Out = [], [], []
	
	# ===========
	#  FUNCTIONS 
	# ===========
	
	# return given string's ANSI-colored version (colors in 0..255 range, are converted to 0..5 range)
	def ansi(s, r, g, b): r, g, b = int(r/255.*5), int(g/255.*5), int(b/255.*5); return "\033[48;5;%dm%s\033[0m" % (16+36*r+6*g+b, s)
	
	# print code with highlighted code pointer and tape position and current cycle information
	def printcode(): n = 20; c = (" "*n)+code+(" "*n); print "%s{... %s\033[48;7;1m%s\033[0m%s ...}; Code pointer: %d; Tape position: {%d, %d, %d}; Cycles: %d" % (cod, c[codepointer-1-n+n:codepointer-1+n], c[codepointer-1+n], c[codepointer+n:codepointer+n+n], codepointer, x, y, z, cycles)
	
	# print tape with highlighted tape position
	def printtape(): print tap + "\n" + "\n".join(["{%s}" % "".join([("\033[38;5;%dm%02X\033[0m" % ((9, 10, 12)[z], tape[z][y][x])) if X == x and Y == y else (ansi("%02X"%tape[z][Y][X], tape[0][Y][X], tape[1][Y][X], tape[2][Y][X])) for X in range(len(tape[0][0])) ]) for Y in range(len(tape[0])) ])
	
	# save tape in ppm file format
	def saveppm(tape, fn): w, h = len(tape[0][0]), len(tape[0]); f = open(fn, "w"); f.write("P3 %d %d 255\n" % (w, h) + "\n".join(["\n".join(["%03d %03d %03d" % (tape[0][y][x], tape[1][y][x], tape[2][y][x]) for x in range(w)]) for y in range(h)])); f.close()
	
	# ==================
	#  INPUT VALIDATION 
	# ==================
	
	# attempt to open source code file
	try   : f = open(parsed.source); rawcode = f.read(); f.close()
	except: print err + "Could not open source code file."; return
	
	# color movement
	colormovement = "*"
	if parsed.colorstar   : colormovement = "*"
	if parsed.colorletters: colormovement = "l"
	if parsed.colorstar and parsed.colorletters: colormovement = "*"; Wrn.append("Cannot use both color star and letters; using color star.")
	
	# cycles / watch
	if parsed.maxcycles <= 0: Wrn.append("Maximum cycles are minimal.")
	if parsed.watchdelay < 0: parsed.watchdelay = 0; Wrn.append("Watch delay less than 0, assuming 0.")
	if parsed.watchskip < 1: parsed.watchskip = 1; Wrn.append("Watch skip less than 1, assuming 1.")
	
	# possible version problem
	if colormovement == "*" and ("r" in rawcode or "g" in rawcode or "b" in rawcode): Wrn.append("Code may require color letters.")
	if colormovement == "l" and  "*" in rawcode                                     : Wrn.append("Code may require color stars."  )
	
	# output file format
	if parsed.output and parsed.output[-4:] != ".ppm": Wrn.append("Output file is saved as a Portable Pixel Map; given file name does not end in .ppm.")
			
	# =============
	#  INTERPRETER 
	# =============

	# simplify code
	if colormovement == "*": code = "".join([c if c in "+-^v<>[].,*"   else "" for c in rawcode])
	if colormovement == "l": code = "".join([c if c in "+-^v<>[].,rgb" else "" for c in rawcode])
	
	# print out simplified code
	if parsed.simplify: print code; return
	# program input
	inputpointer = -1
	inputstring = parsed.input or ""
	outputstring = ""
	
	# pointer to current place in code
	codepointer = 0
	
	# three-dimensional tape
	tape = [
		[[0]],
		[[0]],
		[[0]]
	]
	x, y, z = 0, 0, 0
	
	# number of cycles needed
	cycles = 0
	
	# main loop
	while codepointer < len(code):
		# another cycle needed; fetch char
		cycles += 1; c = code[codepointer]; codepointer += 1
		
		# interpret character
		if   c == "+": tape[z][y][x] = (tape[z][y][x]+1)%256
		elif c == "-": tape[z][y][x] = (tape[z][y][x]-1)%256
		elif c == "^":
			y -= 1
			while y < 0:
				for Z in range(len(tape)): tape[Z].insert(0, [0]*len(tape[0][0]))
				y += 1
		elif c == "v":
			y += 1
			while y >= len(tape[0]):
				for Z in range(len(tape)): tape[Z].append([0]*len(tape[0][0]))
		elif c == "<":
			x -= 1
			while x < 0:
				for Z in range(len(tape)):
					for Y in range(len(tape[0])): tape[Z][Y].insert(0, 0)
				x += 1
		elif c == ">":
			x += 1
			while x >= len(tape[0][0]):
				for Z in range(len(tape)):
					for Y in range(len(tape[0])): tape[Z][Y].append(0)
		elif c == "[":
			if tape[z][y][x] == 0:
				b = 1
				codepointer -= 1
				while b > 0:
					codepointer += 1
					if codepointer >= len(code): print err + "Looping outside source (>)."; return
					if code[codepointer] == "[": b += 1
					if code[codepointer] == "]": b -= 1
				codepointer += 1
		elif c == "]":
			if tape[z][y][x] != 0:
				b = 1
				codepointer -= 1
				while b > 0:
					codepointer -= 1
					if codepointer < 0: print err + "Looping outside source (<)."; return
					if code[codepointer] == "[": b -= 1
					if code[codepointer] == "]": b += 1
				codepointer += 1
		elif c == ".": outputstring += chr(tape[z][y][x])
		elif c == ",": inputpointer += 1; tape[z][y][x] = ord(inputstring[inputpointer]) if inputpointer < len(intputstring) else 0
		elif c == "*": z = (z+1)%3
		elif c == "r": z =  0
		elif c == "g": z =  1
		elif c == "b": z =  2
		
		# watch the program's execution
		if parsed.watch and cycles % parsed.watchskip == 0: printtape(); printcode(); time.sleep(parsed.watchdelay)
		
		# maximum number of cycles
		if cycles >= parsed.maxcycles: Wrn.append("Cycles required exceeded maximum cycles cap (program was halted)."); break
	
	# input length
	if inputpointer >= len(inputstring): Wrn.append("Given input was too short.")
	
	# output
	Out.append(outputstring.encode("string_escape"))
	
	# stop timer, print final status message
	t = time.time()-t0; Inf.append("Taken %d cycle%s and %.2f seconds (%s seconds/cycle)." % (cycles, "s "[cycles == 1], t, "%.8f" % (t/cycles) if cycles != 0 else "/"))
	
	# save final tape
	if parsed.output:
		try   : saveppm(tape, parsed.output)
		except: Wrn.append("Could not save tape.")
	
	# print final tape, warnings, info and output
	if not parsed.silent                 : printtape(); printcode()
	if not parsed.silent and len(Wrn) > 0: print "\n".join([wrn+w for w in Wrn])
	if not parsed.silent and len(Inf) > 0: print "\n".join([inf+i for i in Inf])
	print "\n".join(["%s'%s'" % (out, o) for o in Out])
	
# main	
if __name__ == "__main__": main()
