#!/usr/bin/python
"""
 random_walk.cgi

 A CGI script to generate an image of a random walk
 using the python PIL library.

 final version : a random walk, URL parameters and cookies, oh my.

 Options which can be set in the URL and remembered in a cookie :
    key                    description                                  default
    ------------------     -----------------------------------------    -------
    width                  window width in pixels                        400
    height                 window height in pixels                       400
    steplength             length of random walk step in pixels            8
    maxsteps               maxium number steps (or stop at image edge)  1000
    colorchangeperstep     max (r,g,b) color shift per step               16

    default                if set (i.e. = 1), all other options are
                           reset to their default values.

 Running it over the web :

   First visit it with arguments in the URL :
     http://....../random_walk.cgi?width=200&height=200

   Then successive visits will keep the same size (storing data in a cookie)
     http://....../random_walk.cgi

 Running it from the command line :

   The HTTP headers won't be printed if the REQUEST_METHOD environment 
   variable isn't set, so there are several ways it can be run.

     $ ./random_walk.cgi > rw.png             # write an image to a file

     $ REQUEST_METHOD=GET ./random_walk.cgi   # print HTTP headers and image

   This last approach could also be used to try to 'fake' a web request
   at the command line, by setting for environment variables
   like QUERY_STRING and HTTP_COOKIE explicitly.

 Jim Mahoney | cs.marlboro.edu | Sep 2012 | MIT License
"""
from PIL import Image, ImageDraw
from random import randint
from sys import stdout
from math import sin, cos, pi
import cgi
import Cookie
import os

## --- debugging -----------------------------------------------------
##
## With these next two lines turned on,
## exceptions will be displayed in the returned web page.
## However, compile errors still crash the program - 
## run it from the command line and/or look in the web server log.
##
if False:
    import cgitb    # CGI TraceBack
    cgitb.enable()
##
## To see the sort of run time error that cgitb can display,
## do something like like this to hrow a ZeroDivisionError exception.
##
#if True:
#    a = 1/0         # stop execution with an exception
##
## Here's another debugging trick.
## Since the cgitb routine shows the value of passed arguments,
## you can pass a function anything you want to see, and then
## make it throw an exception. 
## For example, if you turn on this definition, and enable cgitb,
## then invoking debug_show(some_variable) would display its value.
##
if False:
    def debug_show(x):     # use cgitb to see the value of x
        return 1/0
    # some_variable = {'one':1, 'ten':10}
    # debug_show(some_variable)

# These image options (i.e. arguments) can be set by a POST or GET 
# web request, and will be remembered with cookies.
input_keys = ('width', 'height', 
              'steplength', 'maxsteps', 
              'colorchangeperstep')

def default_args():
    """Return the default arguments that specifiy the image to be drawn"""
    return {
        'image_mode': 'RGB',
        'background_color': (224, 224, 232), # (red,green,blue) from 0 to 255
        'line_width': 2,                     # pixels (may be buggy and mean 4)
        'width': 400,                        # of the image, in pixels
        'height': 400,                       
        'steplength': 8,                     # in pixels
        'maxsteps': 1000,                    # walk stops at image edge or this
        'colorchangeperstep': 16,            # maximum delta (red, green, blue)
        }

def args_from_cookie(args = {}):
    """ Return image arguments from cookie else from given args """
    # For each argument (e.g. 'width') the corresponding cookie name 
    # is preceded by "randomwalk_" (e.g. "randomwalk_width")
    cookie = Cookie.SimpleCookie()
    try:
        cookie.load(os.environ['HTTP_COOKIE'])
        for key in input_keys:
            # cookie[] returns a Morsel object;
            # see docs.python.org/library/cookie.html ;
            # its .value is a string, which is converted to integer.
            args[key] = int(cookie['randomwalk_' + key].value)
    except (KeyError, ValueError, TypeError):
        pass       # Just keep going if there's an error.
    return args

def args_from_GET_and_POST(args = {}):
    """ Return image arguments from GET or POST or from given args.
        But if 'default' is set, return default_args() """
    # An example web request would be e.g.
    # http://..../random_walk.cgi?maxsteps=3&steplength=32
    post_or_get = cgi.FieldStorage()
    if post_or_get.has_key('default'):
        return default_args()
    else:
        for key in input_keys:
            try:
                # Again, the .value field has the data.
                args[key] = int(post_or_get[key].value)
            except (KeyError, ValueError, TypeError):
                pass
        return args

def set_cookie_string(args):
    """ Return the string for setting the cookie in the HTTPD response"""
    # e.g. 'Set-Cookie: randomwalk_width=32\r\nSet-Cookie: randomwalk_height=64'
    #
    # By default the cookie Path includes the full URL.
    # Note that this means if the script changes location,
    # there may be duplicate cookies stored with the same names.
    #
    cookie = Cookie.SimpleCookie()
    cookie.clear()        # We're going to put in new values.
    for key in input_keys:
        cookie['randomwalk_' + key] = args[key]
    return cookie.output()

def random_direction_step(distance):
    """ Return random direction (dx,dy) with (dx**2 + dy**2) ~ distance**2 """
    angle = 2 * pi * randint(0,999)/1000.0
    return int(distance * cos(angle)), int(distance * sin(angle))

def make_image(args={}):
    """ Return a PIL Image object representing a random walk. """
    image = Image.new(args['image_mode'], 
                      (args['width'], args['height']), 
                      args['background_color'])
    draw = ImageDraw.Draw(image)
    start_point = (args['width']/2, args['height']/2)
    color = (randint(0,255), randint(0,255), randint(0,255))    
    for i in range(args['maxsteps']):
        delta = random_direction_step(args['steplength'])
        end_point = (start_point[0] + delta[0],
                     start_point[1] + delta[1])
        draw.line((start_point, end_point), fill=color, width=args['line_width'])
        dcolor = args['colorchangeperstep']
        # The tuple() conversion in what follows is required by 
        # draw.line() routine, which wants tuples, not lists.
        # I'm also using a 'python list comprehension' syntax.
        # For example, [i**2 for i in (2,3,4)]  evaluates to [4, 9, 16].
        color = tuple([(color[i] + randint(-dcolor, dcolor)) % 255 \
                           for i in (0,1,2)])
        if end_point[0] < 0 or end_point[0] > args['width'] or \
           end_point[1] < 0 or end_point[1] > args['height']:
            break
        start_point = end_point
    return image


arguments = args_from_GET_and_POST(args_from_cookie(default_args()))
if os.environ.has_key('REQUEST_METHOD'):     # Is this a web request?
    print set_cookie_string(arguments)
    print "Content-Type: image/png\n"
print make_image(arguments).save(stdout, 'PNG')
