# operation_table, put and get
# from chapter 3 (section 3.3.3)
def assoc(key, records):
    return (None
            if is_none(records)
            else head(records)
            if key == head(head(records))
            else assoc(key, tail(records)))
def make_table():
    local_table = llist("*table*")
    def lookup(key_1, key_2):
        subtable = assoc(key_1, tail(local_table))
        if is_none(subtable):
            return None
        else:
            record = assoc(key_2, tail(subtable))
            if is_none(record):
                return None
            else:
                return tail(record)
    def insert(key_1, key_2, value):
        subtable = assoc(key_1, tail(local_table))
        if is_none(subtable):
            set_tail(local_table,
                     pair(llist(key_1, pair(key_2, value)),
                          tail(local_table)))
        else:
            record = assoc(key_2, tail(subtable))
            if is_none(record):
                set_tail(subtable,
                         pair(pair(key_2, value),
                              tail(subtable)))
            else:
                set_tail(record, value)
    def dispatch(m):
        return (lookup
                if m == "lookup"
                else insert
                if m == "insert"
                else "undefined operation -- table")
    return dispatch
operation_table = make_table()
get = operation_table("lookup")
put = operation_table("insert")

# In Source, most functions have a fixed number of arguments.
# (The function llist is the only exception, to this so far.)
# The function apply_in_underlying_javascript allows us to
# apply any given function fun to all elements of the 
# linked list args, as if they were separate arguments
def apply(fun, args):
    return apply_in_underlying_javascript(fun, args)
def add(x, y):
    return apply_generic("add", llist(x, y))
def sub(x, y):
    return apply_generic("sub", llist(x, y))
def mul(x, y):
    return apply_generic("mul", llist(x, y))
def div(x, y):
    return apply_generic("div", llist(x, y))

def attach_tag(type_tag, contents):
    return pair(type_tag, contents)
def type_tag(datum):
    return (head(datum)
            if is_pair(datum)
            else error("bad tagged datum -- type_tag", datum))
def contents(datum):
    return (tail(datum)
            if is_pair(datum)
            else error("bad tagged datum -- contents", datum))
# generic selector functions for complex numbers

def real_part(z):
    return apply_generic("real_part", llist(z))
def imag_part(z):
    return apply_generic("imag_part", llist(z))
def magnitude(z):
    return apply_generic("magnitude", llist(z))
def angle(z):
    return apply_generic("angle", llist(z))
def square(x):
    return x * x

def install_rectangular_package():
    def real_part(z): return head(z)
    def imag_part(z): return tail(z)
    def make_from_real_imag(x, y): return pair(x, y)
    def magnitude(z):
        return math_sqrt(square(real_part(z)) +
                   square(imag_part(z)))
    def angle(z):
        return math_atan2(imag_part(z), real_part(z))
    def make_from_mag_ang(r, a):
        return pair(r * math_cos(a), r * math_sin(a))
    # interface to the rest of the system
    def tag(x):
        return attach_tag("rectangular", x)
    put("real_part", llist("rectangular"), real_part)
    put("imag_part", llist("rectangular"), imag_part)
    put("magnitude", llist("rectangular"), magnitude)
    put("angle", llist("rectangular"), angle)
    put("make_from_real_imag", "rectangular",
        lambda x, y: tag(make_from_real_imag(x, y)))
    put("make_from_mag_ang", "rectangular",
        lambda r, a: tag(make_from_mag_ang(r, a)))
    return "done"
install_rectangular_package()

def install_polar_package():
    # internal functions
    def magnitude(z): return head(z)
    def angle(z): return tail(z)
    def make_from_mag_ang(r, a): return pair(r, a)
    def real_part(z):
        return magnitude(z) * math_cos(angle(z))
    def imag_part(z):
        return magnitude(z) * math_sin(angle(z))
    def make_from_real_imag(x, y):
        return pair(math_sqrt(square(x) + square(y)),
                    math_atan2(y, x))

    # interface to the rest of the system
    def tag(x): return attach_tag("polar", x)
    put("real_part", llist("polar"), real_part)
    put("imag_part", llist("polar"), imag_part)
    put("magnitude", llist("polar"), magnitude)
    put("angle", llist("polar"), angle)
    put("make_from_real_imag", "polar",
        lambda x, y: tag(make_from_real_imag(x, y)))
    put("make_from_mag_ang", "polar",
        lambda r, a: tag(make_from_mag_ang(r, a)))
    return "done"
install_polar_package()

def install_complex_package():
    # imported functions from rectangular and polar packages
    def make_from_real_imag(x, y):
        return get("make_from_real_imag", "rectangular")(x, y)
    def make_from_mag_ang(r, a):
        return get("make_from_mag_ang", "polar")(r, a)

    # internal functions
    def add_complex(z1, z2):
        return make_from_real_imag(real_part(z1) +
                                   real_part(z2),
                                   imag_part(z1) +
                                   imag_part(z2))
    def sub_complex(z1, z2):
        return make_from_real_imag(real_part(z1) -
                                   real_part(z2),
                                   imag_part(z1) -
                                   imag_part(z2))
    def mul_complex(z1, z2):
        return make_from_mag_ang(magnitude(z1) *
                                 magnitude(z2),
                                 angle(z1) +
                                 angle(z2))
    def div_complex(z1, z2):
        return make_from_mag_ang(magnitude(z1) /
                                 magnitude(z2),
                                 angle(z1) -
                                 angle(z2))

    # interface to rest of the system
    def tag(z):
        return attach_tag("complex", z)
    put("add", llist("complex", "complex"),
        lambda z1, z2: tag(add_complex(z1, z2)))
    put("sub", llist("complex", "complex"),
        lambda z1, z2: tag(sub_complex(z1, z2)))
    put("mul", llist("complex", "complex"),
        lambda z1, z2: tag(mul_complex(z1, z2)))
    put("div", llist("complex", "complex"),
        lambda z1, z2: tag(div_complex(z1, z2)))
    put("make_from_real_imag", "complex",
        lambda x, y: tag(make_from_real_imag(x, y)))
    put("make_from_mag_ang", "complex",
        lambda r, a: tag(make_from_mag_ang(r, a)))
    return "done"
install_complex_package()

def make_complex_from_real_imag(x, y):
    return get("make_from_real_imag", "complex")(x, y)
def make_complex_from_mag_ang(r, a):
    return get("make_from_mag_ang", "complex")(r, a)
# coercion support

coercion_list = None

def clear_coercion_list():
    global coercion_list
    coercion_list = None

def put_coercion(type1, type2, item):
    global coercion_list
    if is_none(get_coercion(type1, type2)):
        coercion_list = pair(llist(type1, type2, item),
                             coercion_list)
    else:
        return coercion_list

def get_coercion(type1, type2):
    def get_type1(list_item):
        return head(list_item)
    def get_type2(list_item):
        return head(tail(list_item))
    def get_item(list_item):
        return head(tail(tail(list_item)))
    def get_coercion_iter(items):
        if is_none(items):
            return None
        else:
            top = head(items)
            return (get_item(top)
                    if type1 == get_type1(top) and
                       type2 == get_type2(top)
                    else get_coercion_iter(tail(items)))
    return get_coercion_iter(coercion_list)
def install_python_number_package():
    def tag(x):
        return attach_tag("python_number", x)
    put("add", llist("python_number", "python_number"),
        lambda x, y: tag(x + y))
    put("sub", llist("python_number", "python_number"),
        lambda x, y: tag(x - y))
    put("mul", llist("python_number", "python_number"),
        lambda x, y: tag(x * y))
    put("div", llist("python_number", "python_number"),
        lambda x, y: tag(x / y))
    put("exp", llist("python_number", "python_number"),
        lambda x, y: tag(math_pow(x, y)))
    put("make", "python_number",
        lambda x: tag(x))
    return "done"
install_python_number_package()

def make_python_number(n):
    return get("make", "python_number")(n)

def python_number_to_python_number(n):
    return n
def complex_to_complex(n):
    return n
put_coercion("python_number", "python_number",
             python_number_to_python_number)
put_coercion("complex", "complex",
             complex_to_complex)
def exp(x, y):
    return apply_generic("exp", llist(x, y))

c = make_python_number(4)
d = make_python_number(2)
print(exp(c, d))
