# Python 2.7 code; Jonathan Frech; 1st of December 2017

# All cells can either be empty (0), awake (1) or sleeping (2). In each step:
# * empty -> awake iff cell adjacent to exactly one awake, else empty
# * awake -> sleeping
# * sleeping -> sleeping

# import
import argparse, sys, time, os

# convert a given hsl color intor rgb
def hsl(h, s, l):
	# value range
	h = h%360
	if not (0<=h<360 and 0<=s<=1  and 0<=l<=1): return
	
	c = (1-abs(2*l-1))*s
	hh = h/60.
	x = c*(1-abs(hh%2-1))
	r = g = b = 0
	if 0 <= hh < 1: r, g = c, x
	if 1 <= hh < 2: r, g = x, c
	if 2 <= hh < 3: g, b = c, x
	if 3 <= hh < 4: g, b = x, c
	if 4 <= hh < 5: r, b = x, c
	if 5 <= hh < 6: r, b = c, x
	m = l-c/2.
	return int((r+m)*255.), int((g+m)*255.), int((b+m)*255.)

# main function
def main():
	# start timer
	t0 = time.time()
	
	# command line parsing
	parser = argparse.ArgumentParser(description = "A specific cellular automaton renderer.")
	parser.add_argument("--iterations"   , "-i", type = int, default = 10       , metavar = "N", help = "Number of iterations (initial frame not counted)")
	parser.add_argument("--colorempty"   , "-b", type = str, default = "#ffffff", metavar = "C", help = "Empty cell color (#rrggbb)"                      )
	parser.add_argument("--colorawake"   , "-c", type = str, default = "#aaaaaa", metavar = "C", help = "Awake cell color (#rrggbb)"                      )
	parser.add_argument("--colorsleeping", "-C", type = str, default = "#000000", metavar = "C", help = "Sleeping cell color (#rrggbb)"                   )
	parser.add_argument("--rainbow"      , "-r", action = "store_true"                         , help = "Use rainbow colors (overrides color settings)"   )
	parser.add_argument("--scale"        , "-s", type = int, default = 1        , metavar = "N", help = "Cell square pixel size"                          )
	parser.add_argument("--convert"      , "-m", action = "store_true"                         , help = "Execute ImageMagick's convert command"           )
	parser.add_argument("--delay"        , "-d", type = int, default = 5        , metavar = "N", help = "Delay between frames in output image."           )
	parser.add_argument("--loop"         , "-l", type = int, default = 0        , metavar = "N", help = "Gif loops (0 means for indefinite looping)"      )
	parser.add_argument("--keepfiles"    , "-k", action = "store_true"                         , help = "Do not delete files when converting"             )
		
	parsed = parser.parse_args()
	# TODO: rainbow
	
	# import error
	try: from PIL import Image
	except ImportError: parser.error("Cannot import Python Imaging Library (PIL).")
	
	# parse a hex color
	def parsecolor(c):
		try:
			if c[0] == "#": c = "0x%s"%c[1:]
			c = int(c, 16); return (c>>16)&0xff, (c>>8)&0xff, c&0xff
		except: return
	
	# argument errors
	if parsed.iterations < 1: parser.error("Minimum number of iterations is 1.")
	parsed.colorempty    = parsecolor(parsed.colorempty   )
	parsed.colorawake    = parsecolor(parsed.colorawake   )
	parsed.colorsleeping = parsecolor(parsed.colorsleeping)
	if not parsed.colorempty   : parser.error("Colorempty is not a valid #rrggbb color."           )
	if not parsed.colorawake   : parser.error("Colorawake cell color is not a valid #rrggbb color.")
	if not parsed.colorsleeping: parser.error("Colorsleeping is not a valid #rrggbb color."        )
	if parsed.scale < 1: parser.error("Minimum scale is 1.")
	if parsed.delay < 1: parser.error("Minimum delay is 1.")
	
	# generate file name from frame number
	def filename(f): return "img%0*d.png" % (len(str(n)), f)
	
	# save frame with matrix A, file name number f
	def saveframe(A, f):
		# create image
		for j, a in enumerate(A): pix[j%s, j/s] = (parsed.colorempty, parsed.colorawake, parsed.colorsleeping)[a]
		
		# scale and save image
		try: img.resize((parsed.scale*s, parsed.scale*s)).save(f); return True
		except: return
	
	# frame number, size, area, matrix, image, pixel array
	n = parsed.iterations
	s = 2*n+1; a = s**2
	A = [0]*a; A[a/2] = 1
	img = Image.new("RGB", (s, s)); pix = img.load()
	
	# main loop
	for f in range(n+1):
		# status message
		sys.stdout.write("\r\33[K[%s] %06.2f%%" % ("-\|/"[f/4%4], 100./n*f)); sys.stdout.flush()
		
		# rainbow feature
		if parsed.rainbow: parsed.colorempty = (0, 0, 0); parsed.colorsleeping = hsl(360./n*f, .5, .5); parsed.colorawake = hsl(360-360./n*f, .5, .5);
		
		# frame has to be saved successfully
		if not saveframe(A, filename(f)): print "\r\33[KFailed to save frame number %d.\nRendering aborted." % f; break
		
		# iterate: 0 -> 1 if surrounding cells contain exactly one 1, else 0; 1 -> 2, 2 -> 2
		A = [{0: 1 if [j%s>0 and A[j-1], j%s<s-1 and A[j+1], j/s>0 and A[j-s], j/s<s-1 and A[j+s]].count(1) == 1 else 0, 1: 2, 2: 2}[a] for j, a in enumerate(A)]

	# rendering has finished
	t1 = time.time()
	print "\r\33[KRendering took %.2f seconds." % (t1-t0)
	
	# converting images to gif
	if parsed.convert:
		print "Calling ImageMagick..."
		try: os.system("convert -delay %d -loop %d img*.png out.gif" % (parsed.delay, parsed.loop))
		except: print "Failed to convert frames to gif."
		else:
			print "Cleaning up..."
			for f in range(n+1): os.remove(filename(f))
			t2 = time.time(); print "Converting took %.2f seconds, finished in %.2f seconds." % (t2-t1, t2-t0)
	
# run if main
if __name__ == "__main__": main()
