#!/usr/bin/env python3
from controlfloor import *
from controlfloor.core import AmbiguousElementException, Ops, epsilon, force_list
import argparse
import re
import random
import json
from functools import cmp_to_key
import shlex
import sys

X_SCALE = 0.0814  # 0.1 Makes a pretty qwerty keyboard
Y_SCALE = 0.04546 # Experimentally, this will give a 44px textbox two lines, and a shorter 20px label 1 line

DEBUG = False

precedence_rules = [
   [ "y", "south_of" ],
   [ "borders", "overlaps" ],
]

for op in ("overlaps", "contains", "borders" ):
    for dir in ("NW", "NE", "SW", "SE" ):
        precedence_rules.append( ( op+"__"+dir, op + "__" + dir[0] ) )
        precedence_rules.append( ( op+"__"+dir, op + "__" + dir[1] ) )
        precedence_rules.append( ( "contains", op+"__"+dir ) )
        precedence_rules.append( ( "contains", op+"__"+dir[0] ) )
        precedence_rules.append( ( "contains", op+"__"+dir[1] ) )
        precedence_rules.append( ( "contained_by", op+"__"+dir ) )
        precedence_rules.append( ( "contained_by", op+"__"+dir[0] ) )
        precedence_rules.append( ( "contained_by", op+"__"+dir[1] ) )

        if op == "borders":
            precedence_rules.append( ( op+"__"+dir, "overlaps__"+dir  ) )
            if dir[0] == 'N':
                precedence_rules.append( ( op+"__"+dir, 'above'  ) )
            if dir[0] == 'S':
                precedence_rules.append( ( op+"__"+dir, 'below'  ) )
            if dir[1] == 'E':
                precedence_rules.append( ( op+"__"+dir, 'right_of'  ) )
            if dir[1] == 'W':
                precedence_rules.append( ( op+"__"+dir, 'left_of'  ) )
    for dir in ("N","S","E","W"):
        if op == "borders":
            precedence_rules.append( ( op+"__"+dir, "overlaps__"+dir  ) )
            if dir == 'N':
                precedence_rules.append( ( op+"__"+dir, 'above'  ) )
            if dir == 'S':
                precedence_rules.append( ( op+"__"+dir, 'below'  ) )
            if dir == 'E':
                precedence_rules.append( ( op+"__"+dir, 'right_of'  ) )
            if dir == 'W':
                precedence_rules.append( ( op+"__"+dir, 'left_of'  ) )


#print("\n".join([repr(p) for p in precedence_rules]))
#sys.exit(0)

def locate_element( els, to_find ):
    if to_find.locate_params:
        return to_find.locate_params

    attrs = ('x','y','width','height')
    d = {}

    els = [e for e in els if e.reliable_position and not e.relative and not e.optional and not e.excluded]

    els = sorted(els, key = lambda e: (-e.vertical_overlap(to_find), e.elementType != to_find.elementType, -e.horizontal_overlap(to_find) ) )
    for e in els:
        if "y" not in d and e.y == to_find.y:
            d["y"] = e
        if "height" not in d and e.height == to_find.height:
            d["height"] = e
        #print( "v %d: %d %d %d %d %s" %(e.vertical_overlap(to_find), to_find.y, to_find.maxY, e.y, e.maxY, e.python_variable_name) )
        #print( "h %d: %d %d %d %d %s" %(e.horizontal_overlap(to_find), to_find.x, to_find.maxX, e.x, e.maxX, e.python_variable_name) )
        if e.horizontal_overlap(to_find) > -5:
            if e.tuple == deviceInfo.frame.tuple:
                continue
            if "borders__N" not in d and to_find.borders( e.N ):
                d["borders__N"] = e
            if "borders__S" not in d and to_find.borders( e.N ):
                d["borders__S"] = e
            if to_find.contained_by( e ):
                if "contained_by" not in d:
                   d["contained_by"] = []
                d["contained_by"] = [other for other in  d["contained_by"] if not e.contained_by(other) ]
                for other in d["contained_by"]:
                    if e.contains(other):
                        break
                else:
                    d["contained_by"].append(e)
            if to_find.contains( e ):
                if "contains" not in d:
                   d["contains"] = []
                for other in d["contains"]:
                    if other.contains(e):
                        break
                else:
                    d["contains"].append(e)
            if "overlaps__NW" not in d and to_find.overlaps( e.NW ):
                d["overlaps__NW"] = e
            if "overlaps__NE" not in d and to_find.overlaps( e.NE ):
                d["overlaps__NE"] = e
            if "overlaps__SW" not in d and to_find.overlaps( e.SW ):
                d["overlaps__SW"] = e
            if "overlaps__SE" not in d and to_find.overlaps( e.SE ):
                d["overlaps__SE"] = e
            if "overlaps__N" not in d and to_find.overlaps( e.N ):
                d["overlaps__N"] = e
            if "overlaps__E" not in d and to_find.overlaps( e.E ):
                d["overlaps__E"] = e
            if "overlaps__S" not in d and to_find.overlaps( e.S ):
                d["overlaps__S"] = e
            if "overlaps__W" not in d and to_find.overlaps( e.W ):
                d["overlaps__W"] = e

            if "contains__N" not in d and to_find.contains( e.N ):
                d["contains__N"] = e
            if "contains__E" not in d and to_find.contains( e.E ):
                d["contains__E"] = e
            if "contains__S" not in d and to_find.contains( e.S ):
                d["contains__S"] = e
            if "contains__W" not in d and to_find.contains( e.W ):
                d["contains__W"] = e
            #if "above" not in d and to_find.above(e):
            #    d["above"] = e
            #if to_find.below(e):
            #    d["below"] = e

        if to_find.north_of(e):
            if "north_of" not in d or e.y < d["north_of"].y:
                d["north_of"] = e
        if to_find.south_of(e):
            if "south_of" not in d or e.maxY > d["south_of"].maxY:
                d["south_of"] = e

        if e.vertical_overlap(to_find) > -5:
            if "borders__E" not in d and to_find.borders( e.E ):
                d["borders__E"] = e
            if "borders__W" not in d and to_find.borders( e.W ):
                d["borders__W"] = e
            if "contains__E" not in d and to_find.contains( e.E ):
                d["contains__E"] = e
            if "contains__W" not in d and to_find.contains( e.W ):
                d["contains__W"] = e
        if to_find.left_of( e ):
            if "left_of" not in d or e.x < d["left_of"].x:
                d["left_of"] = e
        if to_find.right_of( e ):
            if "right_of" not in d or e.maxX > d["right_of"].maxX:
                d["right_of"] = e

    #print("++++--")
    #foo = {k:v.python_variable_name for k,v in d.items()}
    #print(repr(foo))
    #print("++++--")

    for k in d:
        d[k] = force_list(d[k])

    for higher, lower in precedence_rules:
        if higher in d and lower in d:
            new_lower = []
            for x in d[lower]:
                if not x in d[higher]:
                    new_lower.append(x)
            if new_lower:
                d[lower] = new_lower
            else:
                del(d[lower])

    for k in d:
        if len(d[k]) == 1:
            d[k] = d[k][0]

    params = {}
    for k,values in d.items():
        if not isinstance(values, list):
            values = [values]
        pieces = k.split("__")
        args = []
        if len(pieces) > 1:
            for v in values:
                args.append(getattr(v,pieces[1]))
        else:
            args = values
        if len(args) == 1:
            args = args[0]
        #if pieces[0] in ['y', 'height', 'left_of', 'north_of']:
        params[pieces[0]] = args

    #print(repr(params.keys()))
    #print(repr(params))

 #   print("------")
 #   print(to_find.id)
 #   foo = {k:v.python_variable_name for k,v in d.items()}
 #   print(repr(foo))
 #   print("------")


    to_find.identified = False
    check = to_find.parent.get(elementTypes=to_find.elementType, unidentified=True,**params)
    if check.python_variable_name != to_find.python_variable_name:
        raise Exception("Wrong element returned?")

    foo = {k:v.python_variable_name for k,v in d.items()}
    #raise Exception(foo)
    return d


# Given a text string label/identifier, guess what a good python
# name *might* be.  May require hand-tweeking, this is expected.
def text_to_id( text ):
    if ' ' in text:
        text = text.replace( ' ', '_' )
    # camel-case to snake-case
    #text = re.sub( '(.)(_*[A-Z][a-z]+_*)', r'\1_\2', text )
    text = re.sub( '(_*[a-z])([A-Z]+_*)', r'\1_\2', text )

    # strip leading non-alphabetic characters
    text = re.sub( '^[^a-zA-Z]+(.*)', r'\1', text )

    # strip internal non-alphanumeric characters with a single underscore
    text = re.sub( '(_*[^a-zA-Z0-9_]_*)', r'_', text )

    text = text.lower().replace( '__', '_' ).strip( "_" )
    return text

# raw objective-c types are a bit verbose, and could be simplified for python
# variable names
def type_to_type( t ):
    if t in ("staticText",):
        return "label"
    if t in ("tabBar",):
        return "tabbar"
    if t in ("textField","secureTextField"): #,"textView"):
        return "textfield"
    return t.lower()

# Build a nice comment that visually shows approximately where each elemement is
# on the screen.
sizes = {}
def to_ascii_art( elements ):
    elements = sorted( elements, key=lambda x: x.width * x.height, reverse=True )

    screenWidth = int(round(deviceInfo.logicalWidth * X_SCALE))
    screenHeight = int(round(deviceInfo.logicalHeight * Y_SCALE))

    grid = [" " for x in range(screenWidth + 1)]
    grid = [list(grid) for y in range(screenHeight + 1)]

    for e in elements:
        if e.unmapped:
            continue
        if e.excluded:
            continue
        x_min = int(round(e.x * X_SCALE))
        x_max = int(round(e.maxX * X_SCALE))
        #if x_max == x_min:
        #    m_max = x_min + 1
        y_min = int(round(e.y * Y_SCALE))
        y_max = int(round(e.maxY * Y_SCALE))

        expected_height = max(int(round(e.height * Y_SCALE)),1)
        if y_max - y_min + 1 > expected_height:
            y_min_loss = (e.y * Y_SCALE) - y_min
            y_max_loss = y_max - (e.maxY * Y_SCALE)
            if y_min_loss > y_max_loss:
                y_min += 1
            else:
                y_max -= 1
            if DEBUG:
                print("%s: %d %d %d %0.1f %0.1f : %0.1f->%0.1f" % (e.ascii_id, y_max, y_min, expected_height, y_min_loss, y_max_loss, e.height, (y_max-y_min+1)/Y_SCALE))
        else:
            if DEBUG:
                print("%s: %d %d %d" % (e.ascii_id, y_max,y_min,expected_height))

        expected_width = max(int(round(e.width * X_SCALE)),1)
        while x_max - x_min + 1 > expected_width and expected_width >= 3:
            x_min_loss = (e.x * X_SCALE) - x_min
            x_max_loss = x_max - (e.maxX * X_SCALE)
            if x_min_loss > x_max_loss:
                x_min += 1
            else:
                x_max -= 1
            if DEBUG:
                print("%s: %d %d %d %0.1f %0.1f : %0.1f->%0.1f" % (e.ascii_id, x_max, x_min, expected_width, x_min_loss, x_max_loss, e.width, (x_max-x_min+1)/X_SCALE))
        if x_max - x_min == 0:
            if x_min > 0:
                x_min -= 0
            x_max += 1
        elif x_max - x_min == 1:
            x_min_loss = (e.x * X_SCALE) - x_min
            x_max_loss = x_max - (e.maxX * X_SCALE)
            if x_min_loss > x_max_loss and x_min > 0:
                x_min -= 1
            else:
                x_max += 1
            if DEBUG:
                print("%s: %d %d %d %0.1f %0.1f : %0.1f->%0.1f" % (e.ascii_id, x_max, x_min, expected_width, x_min_loss, x_max_loss, e.width, (x_max-x_min+1)/X_SCALE))
        elif expected_width >= 3:
            if DEBUG:
                print("%s: %d %d %d" % (e.ascii_id, x_max,x_min,expected_width))

        #if y_max - y_min >= 1:
        #    y_min += 1

        # No effort to display elements to the left and right of the screen
        # We could actually make a layout for a scrolling list that is taller
        # than a single screen, but I find this is of questionable use. (it is
        # trivial to do for y > screenHeight, add rows to grid[], a bit harder for
        # y < 0 ).
        if x_min < 0:
            x_min = 0
        if y_min < 0:
            y_min = 0

        if y_max <= 0:
            continue
        if x_max <= 0:
            continue
        if x_min > screenWidth:
            continue
        if y_min > screenHeight:
            continue

        if y_max > screenHeight:
            y_max = screenHeight
        if x_max > screenWidth:
            x_max = screenWidth

        my_size = (y_min,y_max,x_min,x_max)
        sizes[e.ascii_id] = my_size
        for row in range(y_min, y_max + 1):
            for col in range(x_min, x_max + 1):
                c = e.ascii_id
                if x_max - x_min >= 2:
                    if col == x_min:
                        c = "["
                    elif col == x_max:
                        c = "]"
                #if random.random() < 0.5:
                if grid[row][col] != ' ' and c == e.ascii_id and row % 2 == col % 2: #something here already
                    if grid[row][col] not in ("[","]"):
                        other_size = sizes[grid[row][col]]
                        if other_size == my_size:
                            continue
                grid[row][col] = c

    row_strings = ["".join(row) for row in grid]
    #Drop leading blank rows
    while row_strings and row_strings[0].replace(" ","") == "":
        del(row_strings[0])
    #Drop trailing blank strings
    while row_strings and row_strings[-1].replace(" ","") == "":
        row_strings.pop()
    grid_string = "\n".join([("# " + x).rstrip() for x in row_strings])
    return grid_string

ascii_chars = list("ABCdeFGHIJKLMnoPqRSTUvWXYZ0123456789!@#$%^&*-=+abcDefghijjklmNOpQrstuVwxyz")

element_search_arg_names = (
    "maxDepth",
    "maxChildren",
    "elementTypes",
    "elementIds",
    "rootElementIds",
    "rootElementTypes",
    "matchesElementAtCenter",
    "insideElementAtCenter",
    "containsElementAtCenter",
    "nestsWithElementAtCenter",
    "namedOnly",
    "unnamedOnly",
    "includeAllNamedElements",
    "x1","y1","x2","y2","step",
    "variable_name",  #a convient lie, plucked out before processing,
    "elementsAlongLine", #another lie
)

def dict_to_function_call_string(function_name, d):
    pieces = []
    if not d:
        return function_name+"()"

    for k in element_search_arg_names:
        if k not in d:
            continue
        v = d[k]
        # falsey values never have an effect in the API (0, false, empty string...)
        if not v:
            continue
        if isinstance(v, bool):
            pieces.append("%s = 1" % (k,))
        elif isinstance(v, int):
            pieces.append("%s = %d" % (k, v))
        elif isinstance(v, str):
            pieces.append("%s = %s" % (k, json.dumps(v)))
        elif isinstance(v, list):
            pieces.append("%s = %s" % (k, json.dumps(v)))
        else:
            raise Exception("unhandled data type for key " + k + ":" + repr(type(v))+" " +repr(v))

    if not pieces:
        return function_name+"()"
    return function_name+"( " + (", ".join(pieces)) + " )"

type_counters = {}
def get_next_type_name(type_name):
    if type_name not in type_counters:
        type_counters[type_name] = 1
    else:
        type_counters[type_name] += 1
    return (type_counters[type_name], type_name.upper() + str(type_counters[type_name]))

def generate_class( search_args_list, opts):
    all_elements = []
    element_variable_declarations = []
    element_variable_names = []
    all_elements_dict = {}
    for i, search_args in enumerate(search_args_list):
        suffix = ""
        if len(search_args_list) > 1:
            suffix = str(i+1)
        collection_name = "els"+suffix

        class_collection_name = collection_name
        if search_args["variable_name"]:
            class_collection_name = search_args["variable_name"]
        else:
            class_collection_name = "elements" + suffix

        if search_args["elementsAlongLine"]:
            pieces = search_args["elementsAlongLine"].split(",")
            if len(pieces) != 5:
                raise Exception("--elementsAlongLine expects a list of numbers: x1,y1,x2,y2,step")
            extra_args = {
                "x1"   : int(pieces[0]),
                "y1"   : int(pieces[1]),
                "x2"   : int(pieces[2]),
                "y2"   : int(pieces[3]),
                "step" : int(pieces[4]),
            }
            search_args.update(extra_args)
            # Certain args are incompatible with the function
            for k in ("maxDepth","maxChildren","rootElementIds","rootElementTypes"):
                del(search_args[k])

        del(search_args["elementsAlongLine"])
        del(search_args["variable_name"])

        if 'step' in search_args:
            fn = getElementsAlongLine
        elif search_args.elementIds:
            fn = getElements
        elif search_args.namedOnly:
            fn = getNamedElements
            del(search_args["namedOnly"])
        # If no restrictions are specified, search for a fixed set of types (warning: does not contain "other"....)
        # To get "other" types, specify a name, or an explicit type of "other"
        elif not search_args.elementTypes:
            fn = getInteractiveElements
        else:
            fn = getElements

        elements = fn( **search_args )
        #elements.remove_elements(all_elements)
        has_unidentified_elements = False
        found_types = []

        for e in elements:
            if e in all_elements_dict: #uses __has__ method to catch dups between query responses: identical (type,label,identifier,frame)
                continue
            all_elements_dict[e] = e

            if not e.id:
                has_unidentified_elements = True

            e.search_args = search_args
            e.collection_name = collection_name
            e.parent = elements
            e.y_scaled = int(round(e.y * Y_SCALE))
            e.maxY_scaled = int(round(e.maxY * Y_SCALE))

            if not (e.elementType in found_types):
                found_types.append(e.elementType)
            all_elements.append(e)

        # Don't call the slower getInteractiveElements function if it isn't needed
        if fn == getInteractiveElements and not has_unidentified_elements:
            fn = getNamedElements
#        raise Exception(repr(found_types))

        if fn == getInteractiveElements:
            fn = getElements
            search_args["types"] = sorted(found_types)
            #raise Exception(repr(search_args.__dict__)+ " \n" +repr(sorted(found_types)) )

        element_variable_declarations.append("        " + collection_name+" = " + class_collection_name+ " or " + dict_to_function_call_string(fn.__name__,search_args))

        element_variable_declarations.append("        self." + class_collection_name + " = " + collection_name)
        element_variable_names.append(class_collection_name)
        if len(search_args_list) > 1:
            j = 1
            while j < i:
                c = "els" + str(j)
                element_variable_declarations.append("        %s.remove_elements(%s)" % (collection_name, c))
                j += 1

    elements = all_elements
    if not all_elements:
        raise Exception("Found no elements on page")

    # Order elements top-down, then left-to-right
    elements = sorted(elements, key=lambda e:((e.y,e.x,e.height,e.width,e.elementType)))

    ascii_character_index = 0
    elements_by_python_variable_name = {}

    for e in list(elements):
        e.reliable_position =  True
        e.unmapped = False
        e.relative = False
        e.ambiguous_identifier = False
        e.excluded = False
        e.locate_params = {}
        e.python_variable_name = None
        e.quoted_id = None
        e.by_index = None
        e.is_regex_field = False

    used_regexes = {}
    reordered_elements = []
    original_elements = list(elements)
    for high_priority_fields in fields_used_as_dependencies:
        all_high_priority_fields_processed = not bool(high_priority_fields)
        #print("\nXXXXX  " + repr(high_priority_fields))
        for e in list(elements):
            for target_name,params in constraint_parameters_by_target.items():
                if high_priority_fields and target_name not in high_priority_fields:
                    continue
                if target_name in rename_by_constraint_targets:
                    #if target_name == "share_label":
                    #    print("oooo"+e.id)
                    #    print("oooo"+target_name)
                    #    print("oooo"+repr(params))
                    #    #for x in e.parent:
                    #    #    print(x.id)
                    search_result = e.parent.all(exceptions=0,**params)
                    if search_result and e in search_result:
                        #print("================= " + target_name + " : " + e.id)
                        #print (repr(params))
                        if len(search_result) > 1:
                            raise Exception("Rule failed to identify a unique element: " + repr(search_result))
                        if e.python_variable_name:
                            raise Exception("Rename rule renames an element that already has a name... %s %s" %(target_name, e.python_variable_name))
                        #print("ZZZZ" + " -> " + target_name)
                        e.python_variable_name = target_name
                        e.ambiguous_identifier = False
                        e.quoted_id = "None"
                        del(rename_by_constraint_targets[target_name])
                        break

            python_id = text_to_id(e.id)
            python_type =  type_to_type(e.elementType)


            # Always generate these so we can access any variable by type, even if not displayed...
            (python_type_name_index, python_type_name) = get_next_type_name(e.elementType)

            if python_type_name in opts.rename_fields:
                if e.python_variable_name:
                    raise Exception("You've done something weird with conflicting variable renaming: %s %s" %(python_type_name, e.python_variable_name))
            # Was named positionally relative to a previous element
            if e.python_variable_name:
                python_variable_name = e.python_variable_name
                pass
            elif python_id:
                if opts.noTypeSuffix or python_type.lower() in python_id or e.elementType in e.id:
                    python_variable_name = python_id
                else:
                    python_variable_name = python_id + '_' + python_type
            else:
                e.reliable_position = False
                e.ambiguous_identifier = True
                python_variable_name = python_type_name #get_next_type_name(e.elementType)


            while python_variable_name in elements_by_python_variable_name and elements_by_python_variable_name[python_variable_name] != e:
                e.reliable_position = False
                e.ambiguous_identifier = True
                python_variable_name = python_type_name #get_next_type_name(e.elementType)

            if opts.rename_fields and python_type_name in opts.rename_fields:
                python_variable_name = opts.rename_fields[python_type_name]
                e.by_index = python_type_name_index
                e.by_index = e.parent.all(elementTypes=e.elementType).index(e)
            elif opts.rename_fields and python_variable_name in opts.rename_fields:
                python_variable_name = opts.rename_fields[python_variable_name]

            if high_priority_fields:
                if python_variable_name in high_priority_fields:
                    high_priority_fields.remove(python_variable_name)
                    elements.remove(e)
                    reordered_elements.append(e)
                else:
                    continue
            #print("XXX" + python_variable_name)
            if python_variable_name in rename_by_constraint_targets:
                raise Exception("Encountered a field " + python_variable_name + " that did not match its constraints: " + repr(constraint_parameters_by_target[python_variable_name]) )
            e.python_variable_name = python_variable_name

            if e.python_variable_name in constraint_parameters_by_target:
                e.locate_params = constraint_parameters_by_target[e.python_variable_name]
                #e.relative = 1
                if "elementIds" in e.locate_params:
                    r = e.locate_params["elementIds"]
                    e.quoted_id = repr(r).replace("re.compile","_c",1)
                    del(e.locate_params["elementIds"])
                if "elementTypes" in e.locate_params:
                    del(e.locate_params["elementTypes"])
                del constraint_parameters_by_target[e.python_variable_name]
            #print("YYY"+e.python_variable_name + " " + repr(high_priority_fields))
            elements_by_python_variable_name[e.python_variable_name] = e

            if not e.quoted_id:
                e.quoted_id = json.dumps(e.id)

            if e.quoted_id.startswith("_c(") and e.by_index is not None:
                raise Exception("Fields cannot have a regex when specified by index: %s", e.python_variable_name, e.quoted_id)

            e.quoted_type = json.dumps(e.elementType)

            out_if_bounds = False

            if e.x < opts.minX or e.maxX > opts.maxX or e.y < opts.minY or e.maxY > opts.maxY:
                some_outofbounds=True
            elif e.width == 0 or e.height == 0:
                some_outofbounds=True
            #elif e.frame == deviceInfo.frame:
            #    some_outofbounds=True

            if out_if_bounds:
                elements.remove(e)

            e.frame_pieces = repr(e.tuple).split(",")
            if opts.simple_ascii:
                e.ascii_id = "."
            else:
                e.ascii_id = ascii_chars[ascii_character_index]
            e.optional = opts.default_optional

            ascii_character_index += 1
            if opts.excluded_fields and e.ascii_id in opts.excluded_fields:
                e.reliable_position = False
                e.relative = True
                raise Exception(e.ascii_id)
                elements.remove(e)
                #print("CONTINUE1")
                continue
            if opts.optional_fields and e.python_variable_name in opts.optional_fields:
                e.reliable_position = False
                e.optional = True
                #print("CONTINUE2")
                continue
            if opts.excluded_fields:
                if e.python_variable_name in opts.excluded_fields or (e.identifier and e.identifier in opts.excluded_fields) or (e.label and e.label in opts.excluded_fields):
                    elements.remove(e)
                    #print("CONTINUE3")
                    continue
            if opts.relative_fields:
                if e.python_variable_name in opts.relative_fields or e.ascii_id in opts.relative_fields or e.id and e.id in opts.relative_fields:
                    e.reliable_position = True
                    e.relative = True

            if opts.unmap_field and (e.ascii_id in opts.unmap_fields or e.python_variable_name in opts.unmap_fields):
                e.unmapped = True

            if ascii_character_index == len(ascii_chars):
                ascii_character_index = 0

            if not e.relative:
                e.identified = True

            if e.python_variable_name in constraints_by_source:
                for c in constraints_by_source[e.python_variable_name]:
                    if not c.target in constraint_parameters_by_target:
                        constraint_parameters_by_target[c.target] = {}
                    if c.constraint_type not in constraint_parameters_by_target[c.target]:
                        constraint_parameters_by_target[c.target][c.constraint_type] = e
                    else:
                        if "first_" in c.constraint_type:
                            raise Exception("Cannot have multiple %s rules for one element " %(c.constraint_type,))
                        if not isinstance(constraint_parameters_by_target[c.target][c.constraint_type], list):
                            constraint_parameters_by_target[c.target][c.constraint_type] = [constraint_parameters_by_target[c.target][c.constraint_type]]
                        constraint_parameters_by_target[c.target][c.constraint_type].append(e)
                    if c.element_type:
                        if "elementTypes" not in constraint_parameters_by_target[c.target]:
                            constraint_parameters_by_target[c.target]["elementTypes"] = c.element_type
                        else:
                            if c.element_type != constraint_parameters_by_target[c.target]["elementTypes"]:
                                raise Exception("Cannot rename two variables with different types to the same name...")
                    #print("Target constraints:")
                    #print(repr(constraint_parameters_by_target))

            if not high_priority_fields:
                if not all_high_priority_fields_processed:
                    #print("BREAK: " + e.python_variable_name)
                    break

        if high_priority_fields:
            raise Exception("Could not find fields with dependencies:  " + repr(high_priority_fields))


    #print("QQQQ" + repr(constraint_parameters_by_target))
    elements = reordered_elements + elements

    #print(repr([e.quoted_id for e in reordered_elements]))
#    raise Exception(repr([e.id for e  in reordered_elements]))
#    raise Exception(repr([e.quoted_id for e  in reordered_elements]))
    #raise Exception(repr([e.quoted_id for e  in elements]))

    print("# " + shlex.join(sys.argv[:]))

    # Give the list of --include options needed for all fields not already included by default, so the user can copy them
    # into the script before freezing
    if (not opts.freeze) or opts.all_fields:
        included_names = sorted([e.python_variable_name for e in elements if e.python_variable_name and e.python_variable_name not in opts.implicit_fields and e.python_variable_name not in opts.included_fields and e.python_variable_name not in opts.excluded_fields])
        includes = " ".join(["--include " + n for n in included_names ])
        print("# " + includes)
    else:
        ascii_character_index = 0
        for e in [e for e in original_elements if e.python_variable_name in opts.included_fields or e.python_variable_name in opts.implicit_fields]:
            if opts.simple_ascii:
                e.ascii_id = "."
            else:
                e.ascii_id = ascii_chars[ascii_character_index]
                ascii_character_index += 1
        elements = [e for e in elements if e.python_variable_name in opts.included_fields or e.python_variable_name in opts.implicit_fields]

    # Sprintf width for the variable name
    vlen = max([len(x.python_variable_name) for x in elements if not x.relative and len(x.python_variable_name) <= opts.max_padding ] or [1]) + 1
    # Sprintf width for the id string
    ilen = max([len(x.quoted_id)            for x in elements if not x.relative and len(x.quoted_id)            <= opts.max_padding ] or [1]) + 1
    # Sprinf width for the element type
    tlen = max([len(x.quoted_type)          for x in elements if not x.relative and len(x.quoted_type)          <= opts.max_padding ] or [1]) + 1
    # Widths for x,y,width,height
    xlen = max([len(x.frame_pieces[0])      for x in elements if not x.relative ] or [1]) + 1
    ylen = max([len(x.frame_pieces[1])      for x in elements if not x.relative ] or [1]) + 1
    wlen = max([len(x.frame_pieces[2])      for x in elements if not x.relative ] or [1]) + 1
    hlen = max([len(x.frame_pieces[3])      for x in elements if not x.relative ] or [1])

    #has_relative_optional = False
    #has_standard_optional = False
    #for i,e in enumerate(elements):
    #    if e.relative:
    #        has_relative_optional = has_relative_optional or e.optional
    #    else:
    #        has_standard_optional = has_standard_optional or e.optional

    if len(search_args_list) > 1:
        hash_pos = vlen + ilen + tlen + 29
    else:
        hash_pos = vlen + ilen + tlen + 28

    #if has_standard_optional:
    hash_pos += 4

    if not opts.no_headers and not opts.no_class:
        print("from re import compile as _c")
        print("from controlfloor import *")
    else:
        print("")

    print( to_ascii_art(elements) )
    if not opts.no_class:
        print("class %s(CFAForm):" % (opts.class_name))

    arg_string = ", ".join([x + " = None" for x in element_variable_names])

    print("    def %s(self, %s):" %(opts.function_name, arg_string))

    for d in element_variable_declarations:
        print(d)

    if not opts.no_before_load_check:
        print("        self.before_load_elements(" + (", ".join(["self." + x for x in element_variable_names])) + ")\n")

    for i,e in enumerate(elements):
        frame_string = "%-*s%-*s%-*s%-*s" % (xlen, e.frame_pieces[0] + "," , ylen, e.frame_pieces[1] + ",", wlen, e.frame_pieces[2] + ",", hlen, e.frame_pieces[3])

        if 1 or not e.relative and not e.locate_params:
            #print( '%-*s  # %s:' %(hash_pos, "        # ---- " + e.python_variable_name + " ---- ", e.ascii_id) + frame_string )
            #continue


            include_label = e.label and e.identifier and e.label != e.identifier
            include_label = include_label or e.quoted_id.startswith("_c(") or opts.print_identifiers or e.quoted_id == "None"
            suppress_comments = False
            if opts.no_labels or opts.private_data_field and e.python_variable_name in opts.private_data_field:
                suppress_comments = True

            include_identifier =  e.quoted_id.startswith("_c(") or opts.print_identifiers

            if e.identifier and include_identifier and not suppress_comments:
                print("        # identifier: " + e.identifier)
            if e.label and include_label and not suppress_comments:
                print("        # label: " + e.label)


            if e.relative:
                bounds = locate_element(elements, e)
            else:
                bounds = e.locate_params or {}
            if "elementIds" in bounds:
                del(bounds["elementIds"])
            if "elementTypes" in bounds:
                del(bounds["elementTypes"])
            has_extra_parameters = e.optional or len(bounds) or e.by_index is not None

            #if e.python_variable_name.isupper():
            #    e.relative = True  XYZQ
            comment_char = " "
            comment_pad = ""
            xx = ""
            yy = " "
            if e.ambiguous_identifier:
                if e.by_index is None:
                    xx = "#"
                    yy = " "

            opyy = ""
            opxx = ")" #not optional, suppress exceptions
            if has_extra_parameters:
                opxx = "," #optional, suppress exceptions
                opyy = ","
                opxx = " "

            quoted_id = e.quoted_id
            if len(quoted_id) > ilen:
                quoted_id = "x"
                print( "        x = " + e.quoted_id  )
            #if e.by_index is not None:
            #    x1 =  "None, %s %s, index = %d" % ( e.quoted_type, opxx, e.by_index)
            #    header = '        %s%s%-*s =%s%s.identify( %-*s # %s:' % (xx, "self.", vlen, e.python_variable_name, yy, e.collection_name, vlen+ilen-3, x1, e.ascii_id) + frame_string
            #else:
            header = '        %s%s%-*s =%s%s.identify( %-*s,%-*s%s # %s:' %(xx, "self.", vlen, e.python_variable_name, yy, e.collection_name, ilen, quoted_id, tlen, e.quoted_type+opyy, opxx, e.ascii_id) + frame_string
            print(header)
            paren_idx =  header.index("(") + 2
            paren_spaces = "".ljust(paren_idx)

            if not has_extra_parameters:
                continue

            extra_parameters = {}
            for k in dict(bounds):
                values = force_list(bounds[k])

                pieces = k.split("__")
                if len(pieces) == 1:
                    if k not in extra_parameters:
                        extra_parameters[k] = []
                    for v in values:
                        extra_parameters[k].append( [opts.function_prefix+v.python_variable_name, v] )
                else:
                    if pieces[0] not in extra_parameters:
                        extra_parameters[pieces[0]] = []
                    for v in values:
                        extra_parameters[pieces[0]].append( [opts.function_prefix+v.python_variable_name+"."+pieces[1], v] )

            for k in list(extra_parameters):
                values = extra_parameters[k]
                if len(values) == 1:
                    values[0][0] = values[0][0] + ","
                else:
                    extra_parameters[k] = sorted(extra_parameters[k])
                    for i,v in enumerate(extra_parameters[k]):
                        if i == 0:
                            extra_parameters[k][i][0] = "( " + v[0] + ","
                        elif i == len(extra_parameters[k]) - 1:
                            extra_parameters[k][i][0] = "  " + v[0] + " ),"
                        else:
                            extra_parameters[k][i][0] = "  " + v[0] + ","

            klen = 0
            kvlen = 0
            if extra_parameters:
                klen = max([len(k) for k in extra_parameters])
                kvlen = max( [max([len(q[0]) for q in v]) for v in extra_parameters.values()])

            lines = []
            if e.optional:
                klen = max(klen, len("optional"))
                lines.append( "%s%-*s = True," % (paren_spaces, klen, "optional",) )
            if e.by_index is not None:
                klen = max(klen, len("index"))
                lines.append( "%s%-*s = %d," % (paren_spaces, klen, "index",e.by_index) )
            for k,values in extra_parameters.items():
            #print( '%-*s  # %s:' %(hash_pos, "        # ---- " + e.python_variable_name + " ---- ", e.ascii_id) + frame_string )
                x = '%s%-*s = %-*s' %(paren_spaces, klen, k, kvlen, values[0][0])
                x = '%-*s#%s' %(hash_pos , x, values[0][1].ascii_id)
                lines.append(x)
                for v in values[1:]:
                    x = '%s%-*s   %-*s ' %(paren_spaces, klen, "", kvlen, v[0])
                    x = '%-*s#%s' %(hash_pos, x, values[0][1].ascii_id)
                    lines.append(x)
            if not lines:
                lines = ["AAAAAAAAAAAAAAAAAAA"]
            lines[-1] = lines[-1].replace(",",")")
            print("\n".join(lines))
            #print( '        )')
            e.reliable_position = True


    all_expected_field_names = list(set(list(opts.included_fields) + list(opts.implicit_fields)))
    for e in elements:
        if e.python_variable_name in all_expected_field_names:
            #print(e.python_variable_name)
            all_expected_field_names.remove(e.python_variable_name)
    if not opts.all_fields and all_expected_field_names:
        for f in all_expected_field_names:
            print("Failed to find element included with --include: " + f)
        raise Exception("Missing elements..." + (",".join(all_expected_field_names)))

    for e in elements:
        if e.python_variable_name == "profile_photo_button":
            for f in elements:
                if f.python_variable_name == "create_a_story_button":
                    #print(repr(f.parent.get(overlaps = e.SE)))
                    #print(repr(f.parent.get(contains = f.NW)))
                    pass

    for e in elements:
        if e.python_variable_name == "share_profile_button":
            for f in elements:
                if f.python_variable_name == "share_profile_label":
                    #print(repr(f.parent.get(contains = f, elementTypes="button")))
                    #print(repr(f.parent.get(contained_by = e,elementTypes="staticText")))
                    pass

    for e in elements:
        if e.python_variable_name == "add_bio_button":
            for f in elements:
                if f.python_variable_name == "CELL5":
                    #print(repr(f.parent.get(contains = e.S, elementTypes="cell")))
                    #print(repr(f.parent.get(borders = e.S, elementTypes="cell")))
                    #print(repr(f.parent.get(contained_by = e,elementTypes="staticText")))
                    pass

    #for e in elements:
    #    if e.python_variable_name == "brian_label":
    #        for f in elements:
    #            if f.python_variable_name == "switch_accounts_button":
    #                raise Exception(e.left_of(f))

    # This will automatically test the function just by running the pythong script directly
    if not opts.no_cfrun:
        print('if __name__ == "__main__":')
        if opts.function_name == "load_elements":
            print("    %s.run()" %( opts.class_name,) )
        else:
            print("    %s.run().%s()" % (opts.class_name, opts.function_name))


allow_simultaneous_configs = 4
def add_options( parser ):
    parser.add_argument( "--simple-ascii",   action="store_true", help="class name with parent class, if any" )
    parser.add_argument( "--no-cfrun",       action="store_true", help="class name with parent class, if any" )
    parser.add_argument( "--no-before-load-check",       action="store_true", help="class name with parent class, if any" )
    parser.add_argument( "--no-headers",     action="store_true", help="class name with parent class, if any" )
    parser.add_argument( "--no-class",       action="store_true", help="class name with parent class, if any" )
    parser.add_argument( "--no-els",         action="store_true", help="class name with parent class, if any" )
    parser.add_argument( "--no-labels",    action="store_true", help="class name with parent class, if any" )
    parser.add_argument( "--class-name",     help="class name with parent class, if any", default="DefaultClassName" )
    parser.add_argument( "--function-name",  help="function name to wrap logic inside of", default="load_elements" )
    parser.add_argument( "--function-prefix", default="self.", help="prefix before variable name (self., cls., ...)" )
    parser.add_argument( "--max-padding", default=25, type=int, help="Max number of characters to align with sprintf before overflowing" )
    parser.add_argument( "--debug",   action="store_true", help="Print additional info during class generation" )

    parser.add_argument( "--x-scale", type=float, default=X_SCALE, help="horizontal scaling for ascii layout" )
    parser.add_argument( "--y-scale", type=float, default=Y_SCALE, help="vertical scaling for ascii layout" )
    parser.add_argument( "--minX", type=int, default=0,     help="Only include elements whose x coordinate is >= minX" )
    parser.add_argument( "--maxX", type=int, default=99999, help="Only include elements whose maxY coordinate is <= maxX" )
    parser.add_argument( "--minY", type=int, default=0,     help="Only include elements whose y coordinate is >= minY" )
    parser.add_argument( "--maxY", type=int, default=99999, help="Only include elements whose maxY coordinate is <= manX" )

    parser.add_argument( "--noTypeSuffix", action="store_true", default=False, help = "Never include type name in element variable name" )
    parser.add_argument( "--print-identifiers", action="store_true", default=False, help = "Print all labels and identifiers as comments before each element" )
    parser.add_argument( "--default-optional", action="store_true", default=False, help = "mark all elements as optional" )

    parser.add_argument( "--freeze", action="store_true" )
    parser.add_argument( "--include-field",         "--include",   action="append", help="comma-delimited list of fields to exclude. (python formatted field names)" )
    parser.add_argument( "--exclude-field",         "--exclude",  action="append", help="comma-delimited list of fields to exclude. (python formatted field names)" )
    parser.add_argument( "--all-fields",            "--all",   action="store_true", help="ignore include-field arguments" )
    parser.add_argument( "--optional-field",        "--optional",  action="append", help="comma-delimited list of fields to exclude. (python formatted field names)" )
    parser.add_argument( "--unmap-field",           "--unmap",   action="append", help="comma-delimited list of fields to exclude from ascii graph" )
    parser.add_argument( "--relative-field",        "--relative", action="append", help="field to find relative to other fields, rather than by identifier" )

    parser.add_argument( "--rename-above-field",    "--rename-above",    nargs=3, action="append", help="[name of field before field to be renamed] [fieldname]" )
    parser.add_argument( "--rename-below-field",    "--rename-below",    nargs=3, action="append", help="[name of field before field to be renamed] [fieldname]" )
    parser.add_argument( "--rename-right-of-field", "--rename-right-of", nargs=3, action="append", help="[name of field before field to be renamed] [fieldname]" )
    parser.add_argument( "--rename-left-of-field",  "--rename-left-of",  nargs=3, action="append", help="[name of field before field to be renamed] [fieldname]" )

    parser.add_argument( "--above",nargs=2, action="append", help="" )
    parser.add_argument( "--below",nargs=2, action="append", help="" )
    parser.add_argument( "--right-of",nargs=2, action="append", help="" )
    parser.add_argument( "--left-of",nargs=2, action="append", help="" )
    parser.add_argument( "--east-of",nargs=2, action="append", help="" )
    parser.add_argument( "--west-of",nargs=2, action="append", help="" )
    parser.add_argument( "--north-of",nargs=2, action="append", help="" )
    parser.add_argument( "--south-of",nargs=2, action="append", help="" )


    parser.add_argument( "--rename-field",          "--rename",  nargs=2, action="append", help="[old_name|ascii_id] [new_name]" )

    parser.add_argument( "--regex-rename-field",  "--regex-rename",  nargs=3, action="append", help="sadfsdfsd" )
    parser.add_argument( "--regex-define-field",  "--regex-define",  nargs=3, action="append", help="sadfsdfsd" )

    #parser.add_argument( "--nonblank-field", "--nonblank",  action="append", help="comma-delimited list of fields that whose id is never blank" )

    parser.add_argument( "--suffix-field",       "--suffix",        nargs=2, action="append", help="sadfsdfsd" )
    parser.add_argument( "--prefix-field",       "--prefix",       nargs=2, action="append", help="sadfsdfsd" )
    parser.add_argument( "--regex-field",        "--regex",        nargs=2, action="append", help="sadfsdfsd" )
    parser.add_argument( "--numeric-field",      "--numeric",      action="append", help="sadfsdfsd" )
    parser.add_argument( "--private-data-field", "--private-data", action="append", help="sadfsdfsd" )
    parser.add_argument( "--numeric-abbr-field", "--numeric-abbr",  action="append", help="sadfsdfsd" )

    for i in range(allow_simultaneous_configs+1):
        s = ""
        w = None
        if i > 0:
            s = str(i)
            w = argparse.SUPPRESS

        parser.add_argument( "--variable-name" + s,                      help = w or "class variable name for element query" )
        parser.add_argument( "--elementsAlongLine" + s,                  help = w or "x1,y1,x2,y2,step args for passing to getElementAlongLine() for defining the elment set.")
        parser.add_argument( "--maxDepth" + s,      type=int,            help = w or "snapshot application to specified depth before searching" )
        parser.add_argument( "--maxChildren" + s,   type=int,            help = w or "snapshot application with max children before searching" )
        parser.add_argument( "--elementTypes" + s,                       help = w or "comma-delimited list of types to include")
        parser.add_argument( "--elementIds" + s,                         help = w or "comma-delimited list of types to include")
        parser.add_argument( "--rootElementIds" + s,                     help = w or "search only the children of the specified root element" )
        parser.add_argument( "--rootElementTypes" + s,                   help = w or "search only the children of the specified root element types(should usually accompany --rootId)" )
        parser.add_argument( "--matchesElementAtCenter" + s,             help = w or "include only elements whose frame matches getElementAtPoint", action="store_true", default=False )
        parser.add_argument( "--insideElementAtCenter" + s,              help = w or "include only elements whose frame is inside getElementAtPoint", action="store_true", default=False )
        parser.add_argument( "--containsElementAtCenter" + s,            help = w or "include only elements which contain the result of getElementAtPoint", action="store_true", default=False )
        parser.add_argument( "--nestsWithElementAtCenter" + s,           help = w or "insideElementAtCenter or containsElementAtCenter", action="store_true", default=False  )
        parser.add_argument( "--namedOnly" + s,                          help = w or "get only elements with non-blank label or identifier", action="store_true", default=False )
        parser.add_argument( "--unnamedOnly" + s,                        help = w or "get only elements with blank label and identifier", action="store_true", default=False )
        parser.add_argument( "--includeAllNamedElements" + s,            help = w or "regardless of type, include all elements with non-blank label or identifier", action="store_true", default=False )

#constraints: before, after, right_of, left_of,
all_constraints = []
constraint_graph = {}
constraint_parameters_by_target = {}
rename_by_constraint_targets = {}
constraints_by_source = {}
class Constraint:
    def __init__(self, constraint_type, source, target, element_type = None, rename = False ):
        self.constraint_type = constraint_type
        self.source = source
        self.target = target
        self.rename = rename
        self.depth = None
        self.element_type = element_type
        all_constraints.append(self)

        if rename:
            rename_by_constraint_targets[target]=1

        #if target not in constraints_by_target:
        #    constraints_by_target[target] = []
        if source not in constraints_by_source:
            constraints_by_source[source] = []
        constraints_by_source[source].append(self)
    def __hash__(self):
        return hash( (self.source,self.target,self.constraint_type) )
    def __eq__(self, other):
        return (self.source,self.target,self.constraint_type) == (other.source,other.target,other.constraint_type)
    def __repr__(self):
        return repr((self.source,self.target,self.constraint_type,self.depth))

fields_used_as_dependencies = []

def generate_constraint_graph():
    if not all_constraints:
        fields_used_as_dependencies.append([])
        return
    for c in all_constraints:
        if c.target not in constraint_graph:
            constraint_graph[c.target] = []
        if c not in constraint_graph[c.target]:
            constraint_graph[c.target].append(c)
    changes = 1
    i = 10
    max_depth = 0
    while changes and i > 0:
        i -= 1
        changes = 0
        for c in all_constraints:
            old_depth = c.depth
            if c.source not in constraint_graph:
                c.depth = 0
            else:
                depths = [e.depth for e in constraint_graph[c.source] if e.depth is not None]
                if depths:
                    c.depth = max(depths) + 1
            if old_depth != c.depth:
                changes = 1
    if i == 0:
        print(repr(constraint_graph))
        raise Exception("Failed to compute constraint graph.  Do your dependency rules have a cycle?")

    max_depth = max([c.depth for c in all_constraints])
    for i in range(max_depth+2):
        fields_used_as_dependencies.append([])

    depths = {}
    for c in all_constraints:
        if c.source not in depths:
            depths[c.source] = c.depth
        else:
            depths[c.source] = max(depths[c.source],c.depth)
        #if c.target not in depths:
        #    depths[c.target] = c.depth + 1
        #else:
        #    depths[c.target] = max(depths[c.target],c.depth + 1)

    for c in depths:
        fields_used_as_dependencies[depths[c]].append(c)
    #print(repr(fields_used_as_dependencies))

def run( opts ):
    global X_SCALE,Y_SCALE,DEBUG
    X_SCALE = opts.x_scale
    Y_SCALE = opts.y_scale
    DEBUG = opts.debug
    for k,v in opts.__dict__.items():
        if v is True:
            opts.__dict__[k] = 1

    common_search_args = DictObj()
    search_args_list = []

    # Any field explicity referenced must be found when dumping the element structure, even
    # if it is optional at runtime.
    implicit_fields = {}

    for k,values in opts.__dict__.items():
        if k.endswith("field"):
            if not values:
                continue
            elif k in ("exclude_field", "include_field"):
                continue
            elif k in ("rename_first_field", ):
                implicit_fields[v] = 1
            elif k in ("rename_field", ): #replaced-name kept-name
                for v in values:
                    implicit_fields[v[1]] = 1
                continue
            elif k in ("regex_field", "suffix_field", "prefix_field"):  #field_name  regex
                for v in values:
                    implicit_fields[v[0]] = 1
                continue
            elif k in ("regex_rename_field", ):
                for v in values:
                    implicit_fields[v[2]] = 1
                continue
            for v in values:
                if isinstance(v, list):
                    if len(v) == 3: # [1] is an element type
                        implicit_fields[v[0]] = 1
                        implicit_fields[v[2]] = 1
                    else:
                        implicit_fields[v[0]] = 1
                        implicit_fields[v[1]] = 1
                else:
                    implicit_fields[v] = 1

    opts.implicit_fields = implicit_fields

    for name in element_search_arg_names:
        if name in opts.__dict__:
            common_search_args[name] = opts.__dict__[name]

    if common_search_args['namedOnly'] and common_search_args['unnamedOnly']:
        raise Exception("--namedOnly and --unnamedOnly are incompatible")


    for i in range(1, allow_simultaneous_configs + 1):
        index_used = False
        search_args = DictObj()
        search_args.update(common_search_args)
        for k,v in opts.__dict__.items():
            idx = 0
            try:
                idx = int(k[-1])
            except ValueError:
                continue
            if idx != i:
                continue
            has_numeric_index = 1

            k = k[0:-1]
            if k not in element_search_arg_names:
                raise Exception("Parameter %s does not support multiple configurations" %(k,))
            if v:
                if k == 'unnamedOnly' and 'namedOnly' in search_args and search_args['namedOnly']:
                    search_args['namedOnly'] = False
                if k == 'namedOnly' and 'unnamedOnly' in search_args and search_args['unnamedOnly']:
                    search_args['unnamedOnly'] = False
                search_args[k] = v
                index_used = True
        if index_used:
            search_args_list.append(search_args)

    # If no indexed configs have been specified (ie. --ids3 ...)
    # Then the default config is the only config.
    # BUT, if any indexed config is specified, ONLY indexed configs
    # are used, and non-indexed params are shared defaults
    if not search_args_list:
        search_args_list.append(common_search_args)

    for search_args in search_args_list:
        for name in element_search_arg_names:
            if name != "includeAllNamedElements" and name[-1] == 's': #Lazy; for now all-and-only array-supporting arguments and with the plural 's'
                if not search_args[name]:
                    search_args[name] = None
                else:
                    search_args[name] = [x.strip() for x in search_args[name].split(",") if x.strip()]
    opts.rename_fields = {}
    if opts.rename_field:
        opts.rename_fields = dict(opts.rename_field)

    opts.excluded_fields = opts.exclude_field or []
    for f in opts.excluded_fields:
        if f in opt.implicit_fields:
            raise Exception("Cannot exclude field %s, used by a different rule..." %(f,))


    for x in ("above","below","left_of","right_of","north_of","south_of","east_of","west_of"):
        for (a,b) in opts.__dict__[x] or []:
            implicit_fields[a] = 1
            implicit_fields[b] = 1
            Constraint(x,a,b)
        if x in ("north_of","south_of","east_of","west_of"):
            continue
        y = "rename_" + x + "_field"
        for (a,t,b) in opts.__dict__[y] or []:
            implicit_fields[a] = 1
            implicit_fields[b] = 1
            Constraint("first_" + x, a, b, element_type = t, rename = 1)

    opts.included_fields = opts.include_field or []
    opts.optional_fields = opts.optional_field or []
    opts.unmap_fields = opts.unmap_field or []

    generate_constraint_graph()
    #print(repr(constraint_graph))
    #sys.exit(0)
    for z in fields_used_as_dependencies:
        for x in z:
            if x in opts.optional_fields:
                raise Exception("Cannot have dependencies on optional fields: " + x)

    opts.relative_fields = opts.relative_field or []


    opts.suffix_fields = {}
    if opts.suffix_field:
        opts.suffix_fields = dict(opts.suffix_field)
    opts.prefix_fields = {}
    if opts.prefix_field:
        opts.prefix_fields = dict(opts.prefix_field)
    opts.regex_rename_fields = {}
    if opts.regex_rename_field:
        opts.regex_rename_fields = {re.compile(k):(t,v) for k,t,v in opts.regex_rename_field}

    for (r, t, n) in opts.regex_rename_field or []:
        constraint_parameters_by_target[n] = {"elementIds":re.compile(r), "elementTypes": t}
        rename_by_constraint_targets[n] = 1
    for (n, r) in opts.regex_field or []:
        # Even though "rename" isn't in the --regex-field command, essentially
        # the field must be completely identifyiable by the regex and any other
        # available constraints
        constraint_parameters_by_target[n] = {"elementIds":re.compile(r)}
        rename_by_constraint_targets[n] = 1
    for n in opts.numeric_field or []:
        constraint_parameters_by_target[n] = {"elementIds":re.compile(r"^(\d+)$")}
        rename_by_constraint_targets[n] = 1
    for n in opts.numeric_abbr_field or []:
        constraint_parameters_by_target[n] = {"elementIds":re.compile(r"^([0-9,]+[.]?\d*)([KMkm])?$")}
        rename_by_constraint_targets[n] = 1

#    print(repr(constraints_by_source))
#    print("Target constraints:")
#    print(repr(constraint_parameters_by_target))

    #opts.regex_define_fields = []
    #if opts.regex_define_field:
    #    opts.regex_define_fields = {re.compile(k):(t,v) for k,t,v in opts.regex_define_field}

    opts.regex_fields = {}
    if opts.regex_field:
        opts.regex_fields = {k:re.compile(v) for k,v in opts.regex_field}

    generate_class( search_args_list, opts )
cfrun(__name__, run, add_options )
