#!/usr/bin/python

import random

from math import *
from Gui import *

class World(Gui):

    def __init__(self):
        Gui.__init__(self)
        self.exists = True
        self.animals = []
        self.inter = Interpreter(self, top_globals, top_locals)
        
    def quit(self):
        self.exists = False
        Gui.quit(self)

    def register(self, animal):
        self.animals.append(animal)

    def unregister(self, animal):
        self.animals.remove(animal)

    def clear(self):
        for animal in self.animals:
            animal.undraw()
        self.animals = []
        self.canvas.delete('all')

    def step(self):
        for animal in self.animals:
            animal.step()
        
    def run(self):
        self.running = 1
        while self.exists and self.running:
            self.step()
            self.update()
            time.sleep(self.delay)

    def stop(self):
        self.running = 0

    def map_animals(self, callable):
        map(callable, self.animals)
        
    def run_text(self):
        source = self.te_code.get(1.0, END)
        self.inter.run_code(source, '<user-provided code>')

    def run_file(self):
        filename = self.en_file.get()
        fp = file(filename)
        source = fp.read()
        self.inter.run_code(source, filename)


class Interpreter:

    def __init__(self, world, globals=globals(), locals=locals()):
        self.world = world
        self.globals = globals
        self.locals = locals
        self.globals['world'] = world

    def run_code_thread(self, *args):
        Thread(self.run_code, *args)
        
    def run_code(self, source, filename):
        code = compile(source, filename, 'exec')
        try:
            exec code in self.globals, self.locals
        except KeyboardInterrupt:
            self.world.quit



class TurtleWorld(World):

    def __init__(self):
        World.__init__(self)
        self.delay = 0.1           # time in seconds to sleep after an update
        self.setup()

    def setup(self):
        self.ca_width = 400
        self.ca_height = 400

        # left frame
        self.fr(LEFT)
        self.canvas = self.ca(width=self.ca_width, height=self.ca_height,
                              bg='white')
        self.endfr()

        # right frame
        self.fr(LEFT, fill=BOTH, expand=1)

        self.fr()
        self.bu(LEFT, text='Make Turtle', command=self.make_turtle)
        self.bu(LEFT, text='Print canvas', command=self.canvas.dump)
        self.bu(LEFT, text='Quit', command=self.quit)
        self.endfr()

        self.fr(fill=X)
        self.bu(LEFT, text='Run file', command=self.run_file)
        self.en_file = self.en(LEFT, text='turtle_code.py', width=5,
                               fill=X, expand=1)
        self.endfr()

        self.bu(BOTTOM, text='run this code', command=self.run_text)
        self.te_code = self.te(BOTTOM, height=10, width=40)
        self.te_code.insert(END, 'world.clear()\n')
        self.te_code.insert(END, 'bob = Turtle(world)\n')
        # leave the right frame open so that Turtles can add TurtleControls
        # self.endfr()

    def setup_run(self):
        self.fr()
        self.bu(LEFT, text='Run', command=self.run)
        self.bu(LEFT, text='Stop', command=self.stop)
        self.bu(LEFT, text='Step', command=self.step)
        self.bu(LEFT, text='Clear', command=self.clear)
        self.endfr()

    def make_turtle(self):
        turtle = Turtle(self)
        control = TurtleControl(turtle)


class TurtleControl:

    def __init__(self, turtle):
        self.turtle = turtle
        self.setup()

    def setup(self):
        w = self.turtle.world

        w.fr(bd=3, relief=SUNKEN)
        w.la(text='Turtle Control')

        # ROW 1
        w.fr(fill=X, expand=1)
        w.bu(LEFT, text=' fd ', command=self.fd)
        self.en_r = w.en(LEFT, fill=X, expand=1, width=5, text='10')
        w.bu(LEFT, text='lt', command=self.turtle.lt)
        w.bu(LEFT, text='rt', command=self.turtle.rt)
        w.endfr()

        # ROW 2
        w.fr(fill=X, expand=1)
        w.bu(LEFT, text='pu', command=self.turtle.pu)
        w.bu(LEFT, text='pd', command=self.turtle.pd)

        colors = 'red', 'orange', 'yellow', 'green', 'blue', 'violet'
        self.mb = w.mb(text=colors[0], fill=X, relief=SUNKEN)
        self.mb.pack(pady=1)
        for color in colors:
            w.mi(self.mb, color, command=Callable(self.color, color))

        w.endfr()

        w.endfr()

    def color(self, color):
        self.mb.config(text=color)
        self.turtle.set_color(color)

    def fd(self):
        r = int(self.en_r.get())
        self.turtle.fd(r)

    def turn(self):
        theta = int(self.en_theta.get())
        self.turtle.turn(theta)


class TurmiteWorld(World):

    def __init__(self):
        World.__init__(self)
        self.delay = 0.0           # time in seconds to sleep after an update
        self.ca_width = 500
        self.ca_height = 500
        self.csize = 5
        self.cells = {}
        self.setup()
        self.mainloop()

    def setup(self):

        # left frame
        self.fr(LEFT)
        self.canvas = self.ca(width=self.ca_width, height=self.ca_height,
                              bg='white')
        self.canvas.add_transform(SwirlTransform(pi/400), 0)
        self.endfr()

        # right frame
        self.fr(LEFT, fill=BOTH, expand=1)

        self.fr()
        self.bu(LEFT, text='Make Turmite', command=self.make_turmite)
        self.bu(LEFT, text='Quit', command=self.quit)
        self.endfr()

        self.fr()
        self.bu(LEFT, text='Run', command=self.run)
        self.bu(LEFT, text='Stop', command=self.stop)
        self.bu(LEFT, text='Step', command=self.step)
        self.bu(LEFT, text='Clear', command=self.clear)
        self.endfr()

        self.bu(BOTTOM, text='run this code', command=self.run_text)
        self.te_code = self.te(BOTTOM, height=20, width=40)
        self.te_code.insert(END, 't1 = Turmite(world)\n')
        self.te_code.insert(END, 't2 = Turmite(world)\n')
        self.te_code.insert(END, 't3 = Turmite(world)\n')
        self.te_code.insert(END, 't2.lt()\n')
        self.te_code.insert(END, 't3.rt()\n')
        self.te_code.insert(END, 'world.run()\n')

        # self.endfr()

        low = [-20, -20]
        high = [20, 20]
        # self.make_cells([low, high], **self.unmarked_options)

    def clear(self):
        for animal in self.animals:
            animal.undraw()
        for cell in self.cells.values():
            cell.undraw()
        self.animals = []
        self.cells = {}

    def quit(self):
        self.running = 0
        World.quit(self)

    def get_bounds(self):
        return self.trans.invert_list([[0, 0],
                                       [self.ca_width, self.ca_height]])

    def cell_bounds(self, x, y):
        p1 = [x, y]
        p2 = [x+1, y]
        p3 = [x+1, y+1]
        p4 = [x, y+1]
        bounds = [[p[0]*self.csize, p[1]*self.csize] for p in [p1, p2, p3, p4]]
        return bounds

    def make_cells(self, limits):
        low, high = limits
        for x in range(low[0], high[0]):
            col = []
            for y in range(low[1], high[1]):
                bounds = self.cell_bounds(x, y)
                self.cells[x,y] = Cell(self, bounds)

    def get_cell(self, x, y):
        x, y = int(round(x)), int(round(y))
        cell = self.cells.get((x,y), None)
        if cell==None:
            bounds = self.cell_bounds(x, y)
            cell = Cell(self, bounds)
            self.cells[x,y] = cell
        return cell

    def make_turmite(self):
        turmite = Turmite(self)

# assemble a set of keyword arguments into a dictionary
def makedict(**kwds):
    return kwds

class Cell:
    
    def __init__(self, world, bounds):
        self.world = world
        self.marked_options = makedict(fill='black', outline='gray80')
        self.unmarked_options = makedict(fill='yellow', outline='gray80')
        self.tag = world.canvas.polygon(bounds, **self.unmarked_options)
        self.marked = 0

    def __del__(self):
        self.undraw()

    def undraw(self):
        self.world.canvas.delete(self.tag)
        
    def config_cell(self, **options):
        self.world.canvas.itemconfig(self.tag, **options)

    def cget_cell(self, x, y, option):
        return self.world.canvas.itemconfig(self.tag, option)

    def mark(self):
        self.marked = 1
        self.config_cell(**self.marked_options)
        
    def unmark(self):
        self.marked = 0
        self.config_cell(**self.unmarked_options)
        
    def is_marked(self):
        return self.marked



class Animal:

    def __init__(self, world):
        self.world = world
        self.x = 0
        self.y = 0
        self.delay = 0
        
    def __del__(self):
        self.undraw()

    def step(self):
        pass

    def draw(self):
        pass

    def undraw(self):
        try:
            self.world.canvas.delete(self.tag)
        except AttributeError:
            pass

    def die(self):
        self.world.unregister(self)
        self.undraw()

    def redraw(self):
        self.undraw()
        self.draw()

    def update(self):
        self.world.update()
        time.sleep(self.delay)


class Turtle(Animal):

    def __init__(self, world, delay=0.5):
        Animal.__init__(self, world)
        self.r = 5
        self.heading = 0
        self.pen = 1
        self.color = 'red'
        self.delay = delay
        self.draw()
        world.register(self)

    def step(self):
        self.fd()

    def polar(self, x, y, r, theta):
        rad = theta * pi/180
        s = sin(rad)
        c = cos(rad)
        return [ x + r * c, y + r * s ]         

    def draw_line(self, scale, dtheta, **options):
        r = scale * self.r
        theta = self.heading + dtheta
        head = self.polar(self.x, self.y, r, theta)
        tail = self.polar(self.x, self.y, -r, theta)
        self.world.canvas.line([tail, head], **options)

    def draw(self):
        self.tag = 'Turtle%d' % id(self)
        lw = self.r/2
        self.draw_line(2.5, 0, tags=self.tag, width=lw, arrow=LAST)
        self.draw_line(1.8, 40, tags=self.tag, width=lw)
        self.draw_line(1.8, -40, tags=self.tag, width=lw)
        self.world.canvas.circle(self.x, self.y, self.r, self.color,
                                 tags=self.tag)
        self.update()

    def fd(self, r=1):
        x = self.x
        y = self.y
        p1 = [x, y]
        p2 = self.polar(x, y, r, self.heading)
        self.x = p2[0]
        self.y = p2[1]
        if (self.pen):
            self.world.canvas.line([p1, p2], fill='black')
        self.redraw()

    def bk(self, r=1):
        self.fd(-r)

    def rt(self, angle=90):
        self.heading = self.heading - angle
        self.redraw()

    def lt(self, angle=90):
        self.heading = self.heading + angle
        self.redraw()

    def pd(self):
        self.pen = 1

    def pu(self):
        self.pen = 0

    def set_color(self, color):
        self.color = color
        self.redraw()


class Turmite(Animal):

    def __init__(self, world):
        Animal.__init__(self, world)
        self.dir = 0
        # have to draw yourself before registering!
        self.draw()
        world.register(self)


    def draw(self):
        bounds = world.cell_bounds(self.x, self.y)
        bounds = rotate(bounds, self.dir)
        mid = vmid(bounds[1], bounds[2])
        self.tag = self.world.canvas.polygon([bounds[0], mid,
                                                     bounds[3]], fill='red')

    def fd(self, r=1):
        if self.dir==0:
            self.x += r
        elif self.dir==1:
            self.y += r
        elif self.dir==2:
            self.x -= r
        else:
            self.y -=r
        self.redraw()

    def bk(self, r=1):
        self.fd(-r)

    def rt(self):
        self.dir = (self.dir-1) % 4

    def lt(self):
        self.dir = (self.dir+1) % 4

    def get_cell(self):
        return self.world.get_cell(self.x, self.y)
        
    def mark(self):
        cell = self.get_cell()
        cell.mark()

    def unmark(self):
        cell = self.get_cell()
        cell.unmark()

    def toggle(self):
        if self.is_on_mark():
            self.unmark()
        else:
            self.mark()

    def is_on_mark(self):
        cell = self.get_cell()
        return cell.is_marked()

    def step(self):
        if self.is_on_mark():
            self.lt()
        else:
            self.rt()
        self.toggle()
        self.fd()


class AmoebaWorld(World):

    def __init__(self):
        World.__init__(self)
        self.ca_width = 400
        self.ca_height = 400
        self.animals = []
        self.thread = None

        self.fr(LEFT)
        self.canvas = self.ca(width=self.ca_width, height=self.ca_height,
                              bg='white', transforms=[])
        trans = CanvasTransform(self.canvas, [20, 20])
        self.canvas.add_transform(trans)
        self.endfr()

        # draw the grid
        dash = {True:'', False:'.'}
        (xmin, xmax) = (-10, 10)
        (ymin, ymax) = (-10, 10)
        for x in range(xmin, xmax+1, 1):
            self.canvas.line([[x, ymin], [x, ymax]], dash=dash[x==0])
        for y in range(ymin, ymax+1, 1):
            self.canvas.line([[xmin, y], [xmax, y]], dash=dash[y==0])

    def control_panel(self):
        self.fr(LEFT, fill=BOTH, expand=1)
        
        self.fr(TOP)
        self.bu(LEFT, text='Run', command=self.run_thread)
        self.bu(LEFT, text='Stop', command=self.stop)
        self.bu(LEFT, text='Quit', command=self.quit)
        self.endfr()

        self.fr(fill=X, expand=1)
        self.la(LEFT, text='end time')
        self.en_end = self.en(LEFT, width=5, fill=X, expand=1, text='10')
        self.la(LEFT, text='seconds')
        self.endfr()

        self.en_xoft = self.entry('x(t) = ')
        self.en_yoft = self.entry('y(t) = ')

        self.endfr()

    def entry(self, label):
        self.fr(fill=X, expand=1)
        self.la(LEFT, text=label)
        entry = self.en(LEFT, width=5, fill=X, expand=1, text=' t')
        self.endfr()
        return entry

    def run_thread(self):
        # if there is already a thread, kill it and wait for it to die
        if self.thread:
            self.running = 0
            self.thread.join()

        # find out how long to run
        end = self.en_end.get()
        end = int(end)

        # create a thread and start it
        self.thread = Thread(self.run, end)

    def run(self, end=10):
        self.running = 1
        start_time = time.time()
        t = 0
        while self.exists and self.running and t < end:
            for amoeba in self.animals:
                x = amoeba.xoft(t)
                y = amoeba.yoft(t)
                print 't = %.1f   x = %.1f   y = %.1f' % (t, x, y)
                amoeba.redraw(x, y)
            time.sleep(0.1)
            t = time.time() - start_time
            
    def xoft(self, t):
        return eval(self.en_xoft.get())

    def yoft(self, t):
        return eval(self.en_yoft.get())

        
class Amoeba:

    def __init__(self, world, xoft=None, yoft=None):
        self.world = world
        self.xoft = xoft or self.xoft
        self.yoft = yoft or self.yoft
        self.size = 0.5
        self.color1 = 'violet'
        self.color2 = 'medium orchid'
        world.register(self)

    def xoft(self, t):
        return t

    def yoft(self, t):
        return t

    def undraw(self):
        try:
            self.world.canvas.delete(self.tag)
        except AttributeError:
            pass

    def redraw(self, x, y):
        self.undraw()
        self.draw(x, y)

    def draw(self, x, y):
        thetas = range(0, 360, 30)
        self.tag = 'Amoeba%d' % id(self)

        slime = 'lavender'
        coords = self.poly_coords(x, y, thetas, self.size)
        self.world.canvas.polygon(coords, fill=slime, outline=slime)
        self.world.canvas.polygon(coords,
            fill=self.color1, outline=self.color2, tags=self.tag)

        coords = self.poly_coords(x, y, thetas, self.size/2)
        self.world.canvas.polygon(coords,
            fill=self.color2, outline=self.color1, tags=self.tag)

    def poly_coords(self, x, y, thetas, size):
        rs = [size+random.uniform(0, size) for theta in thetas]
        coords = [self.polar(x, y, r, theta) for (r, theta) in zip(rs, thetas)]
        return coords

    def polar(self, x, y, r, theta):
        rad = theta * pi/180
        s = sin(rad)
        c = cos(rad)
        return [ x + r * c, y + r * s ]         


class GuiAmoeba(Amoeba):

    def xoft(self, t):
        return self.world.xoft(t)

    def yoft(self, t):
        return self.world.yoft(t)


# the following are some random linear-algebra utilities
# written as functions (not methods)

def vadd(p1, p2):
    return [x+y for x,y in zip(p1, p2)]

def vscale(p, s):
    return [x*s for x in p]

def vmid(p1, p2):
    return vscale(vadd(p1, p2), 0.5)

def rotate(v, n=1):
    n %= len(v)
    return v[n:] + v[:n]

# add the turtle functions to the global dictionary
# so they can be invoked as simple functions (not methods)
fd = Turtle.fd
bk = Turtle.bk
lt = Turtle.lt
rt = Turtle.rt
pu = Turtle.pu
pd = Turtle.pd
die = Turtle.die
set_color = Turtle.set_color

# top_globals and top_locals are used to create the
# Interpreter that executes user-provided code.
# It is necessary
# to provide a top-level environment so that user-
# defined functions are top-level functions

top_globals = globals()
top_locals = locals()

if __name__ == '__main__':
    world = TurtleWorld()
    world.mainloop()
