"""This module is part of Swampy, a suite of programs available from
allendowney.com/swampy.

Copyright 2011 Allen B. Downey
Distributed under the GNU General Public License at gnu.org/licenses/gpl.html.
"""

import math
import random
import time

from Tkinter import END
from World import World, Animal, MyThread


class AmoebaWorld(World):
    """A microscope slide where Amoebas trace parametric equations.

    Attributes:
        delay: time step in ms
    """

    def __init__(self, interactive=False, delay=100):
        World.__init__(self)
        self.delay = delay
        self.title('AmoebaWorld')
        self.running = False

        self.make_canvas()
        if interactive:
            self.make_control_panel()

    def make_canvas(self, low=-10, high=10):
        """Makes the canvas and draws the grid marks."""
        self.col()
        self.ca_width = 400
        self.ca_height = 400
        self.canvas = self.ca(width=self.ca_width, height=self.ca_height,
                              bg='white', scale=[20,20])

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

    def make_control_panel(self):
        """Makes the buttons and input fields."""
        # buttons
        self.row()
        self.bu(text='Run', command=self.run)
        self.bu(text='Stop', command=self.stop)
        self.bu(text='Clear', command=self.clear)
        self.bu(text='Quit', command=self.quit)
        self.endrow()

        # end time entry
        self.row([0,1,0], pady=5)
        self.la(text='end time')
        self.en_end = self.en(width=5, text='10')
        self.la(text='seconds')
        self.endrow()

        # entries for x(t) and y(t)
        self.en_x_t = self.make_entry('x(t) = ')
        self.en_y_t = self.make_entry('y(t) = ')

    def make_entry(self, label):
        """Makes an entry with the given label."""
        self.row([0,1,0], pady=5)
        self.la(text=label)
        entry = self.en(width=5, text=' t')
        self.endrow()
        return entry

    def set_entry(self, entry, text):
        """Sets the contents of an entry widget."""
        entry.delete(0, END)
        entry.insert(END, text)

    def set_end_time(self, text):
        """Sets the contents of the end time entry widget."""
        self.set_entry(self.en_end, text)

    def set_x_t(self, text):
        """Sets the contents of the x_t entry widget."""
        self.set_entry(self.en_x_t, text)

    def set_y_t(self, text):
        """Sets the contents of the y_t entry widget."""
        self.set_entry(self.en_y_t, text)

    def run(self):
        """Runs the amoebas in real time."""
        if self.running:
            # after_cancel
            pass

        self.running = True
        self.clear()

        # find out how long to run
        end = self.en_end.get()
        try:
            self.end = float(eval(end))
        except:
            print 'End time must be a numeric expression.'
            return

        self.start_time = time.time()
        self.after(0, self.step)

    def step(self):
        """Advance the Amoebas one step."""
        if not self.exists or not self.running:
            return
        
        xexpr = self.en_x_t.get()
        yexpr = self.en_y_t.get()

        # see how much time has elapsed and evaluate x(t) and y(t)
        t = time.time() - self.start_time

        if t > self.end:
            return

        x = eval(xexpr)
        y = eval(yexpr)
        print 't = %.1f   x = %.1f   y = %.1f' % (t, x, y)

        for amoeba in self.animals:
            amoeba.move(x, y)
        
        # schedule the next step
        self.after(self.delay, self.step)
            
    def clear(self):
        """Clears the amoebas and slime (but not the grid marks)."""
        for animal in self.animals:
            animal.undraw()
        self.canvas.delete('slime')
            
        
class Amoeba(Animal):
    """A soft, round animal that lives in AmoebaWorld

    Attributes:
        size: radius in hash marks
        color1 = color of the cell
        color2 = color of the nucleus
    """
    def __init__(self, world=None):
        Animal.__init__(self, world)

        # size and color
        self.size = 0.5
        self.color1 = 'violet'
        self.color2 = 'medium orchid'
        self.tag = 'Amoeba%d' % id(self)

    def move(self, x, y):
        """Moves the amoeba and redraws."""
        self.x = x
        self.y = y
        self.redraw()

    def draw(self):
        """Draws the Amoeba."""

        # thetas is the sequence of angles used to compute the perimeter
        thetas = range(0, 360, 30)
        coords = self.poly_coords(self.x, self.y, thetas, self.size)

        slime = 'lavender'

        # draw the slime outline which will be left behind
        self.world.canvas.polygon(coords, fill=slime, outline=slime,
                                  tags='slime')

        # draw the outer perimeter
        self.world.canvas.polygon(coords,
            fill=self.color1, outline=self.color2, tags=self.tag)

        # draw the perimeter of the nucleus
        coords = self.poly_coords(self.x, self.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):
        """Computes coordinates of a polygon with random variation.

        Args:
            x, y: center point
            thetas: sequence of angles
            size: minimum radius; actual radius is up to 2x bigger
        """
        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


if __name__ == '__main__':
    # create the GUI
    world = AmoebaWorld(interactive=True)
    world.set_end_time('2 * math.pi')
    world.set_x_t('10 * math.cos(t)')
    world.set_y_t('10 * math.sin(t)')

    # create the amoeba
    amoeba = Amoeba()
    
    # wait for the user to do something
    world.mainloop()
