#! /usr/bin/env python
import os
import sys
import Image
import whrandom
import time
import string
import getopt
import re
import config # config.py(c) holds configuration information

# TODO:
# Code:
#  randomize sequence of letters, append images together (done)
#  randomize based on string pattern of upper/lowercase/spaces (done)
#  do something about the excessive amount of memory this takes up (partially done)
#  make geminates match (done)
# Images:
#  b extender (done)
#  k (done)
#  ? (done)
#  t tail (done)
#  l/1 serif (done)
#  numbers (done), diagonals (done), crosses (done)
#  fancy v serif
#  slanted n serif
#  questionmark dot
#  half-glottal
#  chi(funny x)
#  phi(o with vert. bar)
#  uppercase (done)
#  X / diag. (done)
#  Y w/ vert. descender
#  2
#  4,5 (upper & lower)

# BUGS:
#  j broken (fixed?)
#  * 3 -> e (fixed?)
#  m -> crv + 5 (fixed?)
#  m hangs over the edge   | Going to ignore these until the program is migrated
#  triple m, triple w      | to vector graphics/object-orientedhood
#  transparency is screwy

# CHANGES:
#  Jan. 16-26, 2002
#  Added uppercase
#  Added alphabet multicharacter escape support
#  Added -a option to print all characters defined in alphabet

syntax   = config.syntax
alphabet = config.alphabet
units    = config.units
font     = config.font

cmd = {}

def parseinputstr( str ):                                # parse input string, separating out input characters
    out = []                                             # from multicharacter escapes '\foo'
    state = " "
    key = ''
    for char in str:
        if state == " ":
            if char == "\\": state = char
            else:            out.append( char )
        elif state == "\\":
            if char == " " or char == "\\":
                out.append( key )
                key = ''
                state = char
            else:            key = key + char
    if key :
        out.append( key )
    return out

def mxfm( image, stack ):                                # returns possibly transformed image
    if ( stack[0] == "-" ):                              # top-bottom flip
        tbimage = image.transpose(Image.FLIP_TOP_BOTTOM)
        stack.pop( 0 )
    else: tbimage = image
    if ( stack[0] == "|" ):                              # left-right flip
        lrimage = tbimage.transpose(Image.FLIP_LEFT_RIGHT)
        stack.pop( 0 )
    else: lrimage = tbimage
    return lrimage

def comparerule( rule, nodes ):                          # compare node list to nodes in rule
    for i in range( 0, len(nodes)):                      # range( a, b ) = (a, a+1, a+2 ... b-2, b-1)
        if (nodes[i] == rule[i][0]):
            pass
        else: return 0
    return 1

def findrule( state, nodes ):                            # find the rule which generated this subtree
    ruleset = syntax[state][1]
    nodelen = len(nodes)
    for rule in ruleset:
        rulelen = len(rule)
        if ((rulelen == nodelen) and (comparerule( rule, nodes ))):
            return rule
    return 

def generate( state ):                                   # generate a random tree (in stack form)
    stack  = [ state ]
    if ( len(syntax[state]) == 1 ):                        # if this is a stop symbol
        return stack
    else:
        stack.append( "[" )
        path = whrandom.randint(0, (len(syntax[state][1])-1)) # choose randomly from next states
        for symbol in syntax[state][1][path]:            # recurse down each non-terminal
            if ( symbol != 0 ):                          # 0 denotes end of list ###
                substack = generate( symbol[0] )         # get subtree
                for elt in substack:       
                    stack.append( elt )
                if (symbol[3]):stack.append( "-" )       # top-bottom flip
                if (symbol[4]):stack.append( "|" )       # left-right flip
            else:
                print "found end of list in generate( state =", state, ")" # this should be deprecated/never happen
        stack.append("]")
        return stack

def draw( stack ):                                       # draw a character based on a tree stack
    state = stack.pop(0)
    #print state,
    image = Image.open( font+syntax[state][0] )          # load the image
    if (stack[0] != "["):                                # terminal stack element
        if (len(syntax[state]) == 1):                      # this state is a terminal node
            return image
        else:
            #print state,"nonterminal! Randomizing letter." # randomizing subtree from this point
            substack = generate( state )                 # generate random substack
            return draw( substack )                      # draw random substack
    else:
        if (stack[0] == "["):
            #print "[",
            stack.pop(0)
            images = []                                  # list of daughter images
            nodes  = []                                  # list of daughter names
            while (stack[0] != "]"):                     # for all nodes in stack
                newstate = stack[0]                      # the new state
                newimage = draw( stack )                 # draw the daughter state
                if (newimage):
                    tfimage = mxfm( newimage, stack )    # maybe transform daughter state
                    images.append( tfimage )             # list of daughter images
                    nodes.append( newstate )             # list of daughter nodes
                else:
                    print "recurse on",newstate,"failed" # this should never happen
                    return None
            rule = findrule( state, nodes )              # find the rule for this subtree
            for i in range( 0, len(images)):
                currimg = images[i]
                box = currimg.getbbox()                  # get bounding box
                dx = rule[i][1]*units
                dy = rule[i][2]*units
                region = currimg.crop(box)               # crop the image
                newbox = ((box[0]+dx),(box[1]+dy),(box[2]+dx),(box[3]+dy))
                image.paste( region, newbox, region )    # paste daughter img. into parent img.
            #print "]",
            stack.pop( 0 )
        return image

def draw_crop_scale( stack ):                            # draw, crop and scale letter image
    image = draw(stack)
    bbox = image.getbbox()            
    newbox  = (bbox[0], 0, bbox[2], image.size[1])       # crop left and right edges
    cimage  = image.crop( newbox )                       # cropped image
    image   = None                                       # saving memory?
    newsize = (cimage.size[0]*cmd['z']/units, cimage.size[1]*cmd['z']/units ) 
    simage  = cimage.resize( newsize, Image.BICUBIC )    # scale down image
    cimage  = None                                       # saving memory?
    return simage
    
def randomize_input_string( str ):                       # generate list of images based on -r input string
    imagelist = []
    str = parseinputstr( str )                           # parse out multichar keys '\foo' from input string
    #print str
    for i in range(0,len(str)):
        char = str[i]
        #if ( re.match("[a-zA-Z0-9?]", char)):
        if ( alphabet.has_key(char)):
            if ((i > 0) and (char == str[i-1])):         # if this letter matches previous letter
                imagelist.append(imagelist[len(stack)-1])# make them the same image
            else:                                        # generate image for letter
                stack = string.split( alphabet[char][whrandom.randint(0,(len(alphabet[char])-1))] , "." )
                #stack = string.split( alphabet[char][whrandom.randint(0,(len(alphabet[char])-2))] , "." ) 
                imagelist.append( draw_crop_scale( stack ))
        elif( char == " "):                              # add a " " space to the image list
            imagelist.append( " " )
        else:                                            # this character is not in config.alphabet, skip it
            print "bad character", char 
    return imagelist

def generate_random_string( str ):                       # generate list of images based on -g input string
    imagelist = []
    outputlist = []
    for char in str:
        if ( char == " "):                               # add a " " space to the image list
            imagelist.append( " " )            
        else:
            if ( re.match("[a-z]", char )):              # generate lowercase letter
                stack = generate("lc")
            elif ( re.match("[A-Z]", char )):            # generate uppercase letter
                stack = generate("UC")
            else:                                        # this character is not in config.alphabet, skip it
                print "bad character", char
                stack = generate("start")
            imagelist.append( draw_crop_scale( stack ))
        
    return imagelist

def optikern( image ):                                   # optical kerning algorithm
    left  = []
    right = []

    for i in range( 0, 18 ):
        region = image.crop( (0,  i*cmd['z'], image.size[0],  (i+1)*cmd['z']) )
        width  = region.size[0]
        box    = region.getbbox()
        region = None                                    # hopefully this step saves memory
        if (box):
            left.append( box[0] )                        # distance from left edge of region to left edge of bbox
            right.append( width - box[2] )               # distance from right edge of region to right edge of bbox
        else:
            left.append(  width )
            right.append( width )

    return (left, right)

def layoutstring( imagelist, name ):                     # layout string of letter-images using optical kerning
    kernlist  = []
    length = cmd['z']
    for image in imagelist:
        if (image == " "):                               # leaving room for " " space characters
            length = length + (cmd['z']*config.space)
        else:
            length = length + image.size[0] + cmd['z']   # add letter length to overall length
            kernlist.append( optikern(image) )           # append kerning data for this image

    workspace = Image.new( "RGBA", (length, imagelist[0].size[1]), (0,0,0,255) )

    position = cmd['z']
    for i in range(0, len(kernlist)):
        while(imagelist[i] == " "):
            position = position + (cmd['z']*config.space )
            imagelist.pop(i)

        # set the kerning
        if ((i == 0)): kern = 0                          # for first image, kerning is zero
        else:
            kerncompare = []                             # kerning comparison array
            for j in range( 0, len(kernlist[i][0])):
                kerncompare.append( kernlist[i][0][j]+kernlist[i-1][1][j] )
            kern = min( kerncompare )

        position = position - kern                       # move position back by kern amount
        box = (position, 0, (imagelist[i].size[0]+position), imagelist[i].size[1])
        workspace.paste(imagelist[i], box, imagelist[i]) # paste letter into workspace
        position = position+imagelist[i].size[0]+cmd['z']# advance position by letter width
        imagelist[i] = None                              # matt, the erstwhile memory saver

    output = workspace.crop( (0, 0, position, workspace.size[1]) ) # cut off excess space at end of image
    workspace = None                                     # matt, the erstwhile memory saver
    output.convert("L").save( name+".png" )              # convert to greyscale and save in file "name"

def generate_letter( stack, name ):
    if (os.access( name+".png", 0)):                     # does the file exist?
        print "File \""+name+".png\" already exists. Skipping."
    else:
        output = draw_crop_scale( stack )
        output.convert("L").save( name+".png" )          # convert to greyscale and save in file "name"
        
def alphabetout():                                       # print all character names defined in the alphabet
    keys = []
    for v in alphabet.keys(): keys.append( v )
    keys.sort()
    return str(len(keys))+" characters total in the alphabet:\n"+string.join( keys, "\t " )

def countforms( state ):                                 # count the number of possible letters generated by state
    r = 0
    if ( len(syntax[state]) == 1 ):
        return 1
    for rule in syntax[state][1]:
        if ( rule != 0 ):
            n = 1
            for node in rule:
                if (node != 0): ###
                    n = n * countforms( node[0] )
                else:
                    print "found node=0 in countforms(", state, ")" # this should be deprecated/should never happen
            r = r + n

    return r

def getargs( args ):                  # read command line arguments
    for i in range(0, len(args)):
        if (args[i] == "-a"):         # -a print the character names in the alphabet
            cmd['a'] = 1
        elif (args[i] == "-c"):       # -c count number of possible letters
            cmd['c'] = 1
        elif (args[i] == "-g"):       # -g generate random string based on input
            cmd['g'] = args[i+1]
        elif (args[i] == "-h"):       # -h help!
            cmd['h'] = 1
        elif (args[i] == "-n"):       # -n number of files to generate
            if (int(args[i+1]) > 0):
                cmd['n'] = abs(int(args[i+1]))
            else: print "invalid number", args[i+1]
        elif (args[i] == "-o"):       # -o output file
            cmd['o'] = args[i+1]
        elif (args[i] == "-r"):       # -r randomize input string 
            cmd['r'] = args[i+1]
        elif (args[i] == "-s"):       # -s start state
            if (syntax.has_key( args[i+1] )):
                cmd['s'] = args[i+1]
            else: print "invalid state", args[i+1]
        elif (args[i] == "-t"):       # -t tree to draw
            cmd['t'] = args[i+1]
        elif (args[i] == "-z"):       # -z (zoom) size of image 1-36 (pixels per unit square)
            if ((int(args[i+1]) <= 36)and(int(args[i+1]) >= 0)):
                cmd['z'] = abs(int(args[i+1]))
            else: print "invalid scale factor", args[i+1]

    if (not (cmd.has_key('s'))):      # make the start state "start" if it's undefined
        cmd['s'] = "start"
    if (not (cmd.has_key('n'))):      # make the number of files to generate 1 if it's undefined
        cmd['n'] = 1
    if (not (cmd.has_key('z'))):      # scale factor in range(1, config.units)
        cmd['z'] = config.units
    
    return cmd

def main():
    cmd = getargs( sys.argv )
    if cmd.has_key('a') :                                # print all character names defined in the alphabet
        print alphabetout()
    elif ( cmd.has_key('c') ):                           # count number of possible letterforms
        print countforms( cmd['s'] ), "possible letterforms."
    elif ( cmd.has_key('h') ):                           # print help
        print config.help
    elif ( cmd.has_key('r') ):                           # randomize input string (maintain readability)
        for i in range(0, cmd['n']):
            if (cmd.has_key('o')): name = cmd['o']+str(i)
            else:                  name = "./output/"+cmd['r']+str(i)
            print "%d: randomizing input string..." % i
            imagelist = randomize_input_string( cmd['r'] )
            print "%d: laying out string..." % i
            layoutstring( imagelist, name )
    elif ( cmd.has_key('g') ):                           # generate random string based on input (unreadable)
        for i in range(0, cmd['n']):
            stack = generate( cmd['s'] )
            if (cmd.has_key('o')): name = cmd['o']+str(i)
            else:                  name = "./output/"+cmd['g']+str(i)
            print "%d: generating random string..." % i
            imagelist = generate_random_string( cmd['g'] )
            print "%d: laying out string..." % i
            layoutstring( imagelist, name )
    elif (cmd.has_key('t') ):
        stack = string.split(re.sub("\\.$","",
                                     re.sub("\\.\\.",".",
                                             re.sub(",",".", 
                                                     re.sub("(\\[|\\]|\\||-)",".\\1.",cmd['t'],0),0),0),0),".")
        if (cmd.has_key('o')): name = cmd['o']+str(i)
        else:                  name = "./output/"+string.join(stack,"")
        generate_letter( stack, name )
    elif (cmd.has_key('s') ):                            # generate random letter starting from state
        for i in range(0, cmd['n']):
            stack = generate( cmd['s'] )
            if (cmd.has_key('o')): name = cmd['o']+str(i)
            else:                  name = "./output/"+string.join(stack,"")
            generate_letter( stack, name )
            print "%d," % i,
    else:
        print "Something very bad happened."

main()
