# Author: Natan Zohar
import gtk
import math
import pango
import random
import gobject
import cairo

VERSION=0.01
# Initialize threading support so we don't crash.
gtk.gdk.threads_init()
LOWER=0
UPPER=1000.0

# Written by Jacob Martin
def circleproblem((a,b), angle):
#    angle = angle * math.pi / 180
    r = math.sqrt(a**2 + b**2)
    x = r * math.cos(angle)
    y = r * math.sin(angle)

    return (int(x), int(y))

def circle_compare(a, b):
    return b[1] - a[1]

# Class for choosing and keeping track of random colors
class ColorPicker:
    def __init__(self):
        self.chosen = []
        random.seed(None)
        
    def pickRandom(self):
        return random.randint(LOWER,UPPER)/UPPER, \
        random.randint(LOWER,UPPER)/UPPER, \
        random.randint(LOWER,UPPER)/UPPER
        
    def nextColor(self):
        similar = True
        threshhold = .3
        while similar is True:
            r,g,b = self.pickRandom()
            if r > 1-threshhold or r < threshhold:
                continue
            if g > 1-threshhold or g < threshhold:
                continue
            if b > 1-threshhold or b < threshhold:
                continue
            similar = False
            for color in self.chosen:
                if self.isSimilar((r,g,b), color):
                    similar = True
                    break
        self.chosen.append((r,g,b))
        return (r,g,b)

    def clearColors(self):
        self.chosen = []
        
    def isSimilar(self, (x,y,z), (a,b,c)):
        sim_quotient = .1
        sim_quotient -= abs(x-a)
        sim_quotient -= abs(y-b)
        sim_quotient -= abs(z-c)
        return sim_quotient >= 0

# Whenever someone talks, what will happen is this:
# - first, that person is added to the conversation queue
# - next, if the length of people talking has been reached,
#   the first person on teh queue will be decremented.
# - that person's conversant value is incremented.

# We judge a persons participation in the active conversation
# by how many lines he has posted recently, we can tell how
# many he has posted by checking the map conversants.
# Additionally, we should be able to tell who he is addressing
# and group them together if they have been talking to each other.

class Tracker:
    def __init__(self, qlen=30):
        self.QUEUE_LENGTH = qlen
        self.queue = []
        self.conversants = {}
        self.sorted = []

    def handle_talk(self, person):
        # Prepend to the queue and pop the last element off
        self.queue.insert(0,person)
        self.queue = self.queue[:self.QUEUE_LENGTH]

        # increment the person who just talked speech factor
        if person in self.conversants:
            self.conversants[person] += 1
        else:
            self.conversants[person] = 1

    # Somehow get the rating of the person in the current conversation.
    # Perhaps it is a function of how many lines a person has said as well
    # as their most recent position in the queue (first occurrence from the back).
    
    # the representation of a person may also be more interesting, perhaps they will be 
    # represented by a blob (bigger means more lines spoken) and the more recent the
    # last thing they said was, the closer to the center of the circle they will be.

    # return an int representing where in the queue they last spoke and 
    # an int, representing how many spots in the queue they take up.
    def get_rating(self, person):
        try:
            return (self.queue.index(person), self.queue.count(person)) 
        except ValueError:
            return (-1, 0)


class Blobber(gtk.DrawingArea):
    def __init__(self, qlen=30):
        gtk.DrawingArea.__init__(self)
        self.__trackers = {}
        self.__colorpickers = {}
        self.__colordicts = {}
        self.hash = None
        self.qlen = qlen

        self.__circles = [] # fill with ((x,y), radius, color) tuples
        self.__names = [] # fill with ((x,y), text, color, angle, size) tuples
        self.__initWidgets()
        self.__connectHandlers()

    def __initWidgets(self):
        # We create a signal called 'blob-clicked' whenever a blob on our widget is actually clicked. How will we find out? 
        # it's a good question.
        try:
            gobject.signal_new("blob-clicked", self, gobject.SIGNAL_RUN_LAST, \
                gobject.TYPE_BOOLEAN, (gtk.gdk.Event, gobject.TYPE_STRING, ))
        except:
            pass
        self.add_events(gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.BUTTON_RELEASE_MASK)

    def __connectHandlers(self):
        self.connect('expose-event', self.expose_event_cb)
        self.connect('size-allocate', self.resize_cb)
        self.connect('button_release_event', self.button_press_cb)

    # Misc. Functions
    def addCircle(self, (x,y), diameter, filled=False, color=(0,0,0)):
        self.__circles.append(((x,y), diameter/2, color))

    def addText(self, (x,y), text, color=(0,0,0), angle=0, size=0):
        self.__names.append(((x,y), text, color, angle, size))

    def re_color(self, hash=None):
        try:
            tracker = self.__trackers[hash]
        except:
            tracker = self.add_hash(hash)
        self.__colordicts[tracker] = {}
        self.__colorpickers[tracker].clearColors()
        self.visualize(hash)
        self.queue_draw()

    def add_blob(self, text, hash):
        try:
            tracker = self.__trackers[hash]
        except KeyError:
            tracker = Tracker(self.qlen)
            self.__trackers[hash] = tracker
        try:
            colorpicker = self.__colorpickers[tracker]
        except KeyError:
            colorpicker = ColorPicker()
            self.__colorpickers[tracker] = colorpicker

        tracker.handle_talk(text)

        if self.hash == hash:
            self.visualize(hash)
            self.queue_draw()

    def add_hash(self, hash):
        tracker = Tracker(self.qlen)
        self.__trackers[hash] = tracker
        self.__colordicts[tracker] = {}
        self.__colorpickers[tracker] = ColorPicker()
        return tracker

    def show_hash(self, hash):
        self.hash = hash
        self.queue_draw()

    def clear_hash(self, hash):
        tracker = self.__trackers[hash]
        del self.__trackers[hash]
        del self.__colordicts[tracker]
        del self.__colorpickers[tracker]
        self.add_hash(hash)

    def list_hashes(self):
        return self.__trackers.keys()

    def save_to_png(self, pngfile):
        context = self.window.cairo_create()
        surface = context.get_target()
        surface.write_to_png(pngfile)

    def visualize(self, hash=None):
        if hash is None:
            hash = self.hash
        try:
            tracker = self.__trackers[hash]
        except KeyError:
            tracker = self.add_hash(self.hash)
        self.__circles = []
        self.__names = []
        nicks = dict.fromkeys(tracker.queue).keys()
        totalnicks = len(nicks)
        curnickindex = 1
        # we have to place the bubbles around the center circle.
        # find out the total number of nicks. place the first nick at 0 degrees
        # place the next at 360/totalnick + prev degree and so on
        for nick in nicks:
            distance, size = tracker.get_rating(nick)
            try:
                color = self.__colordicts[tracker][nick]
            except KeyError:
                color = self.__colorpickers[tracker].nextColor()
                try:
                    self.__colordicts[tracker][nick] = color
                except KeyError:
                    self.__colordicts[tracker] = {}
                    self.__colordicts[tracker][nick] = color

            if distance is -1:
                curnickindex += 1
                continue
            offset = (distance+1)*float(self.diameter)/tracker.QUEUE_LENGTH/2
            angle = 360.0/totalnicks * curnickindex * math.pi / 180.0
            xoff, yoff = circleproblem((offset, 0), angle)
            xoff += self.x/2
            yoff += self.y/2
            diameter = size*self.diameter/tracker.QUEUE_LENGTH/3
            self.addCircle((xoff,yoff), diameter, True, color)
            self.addText((xoff, yoff), nick, color, angle, diameter/2)
            curnickindex += 1
        self.__circles.sort(circle_compare)


    # Signal Handlers
    def expose_event_cb(self, widget, event):
        area = event.area
        self.visualize()
        self.context = self.window.cairo_create()
        self.surface = self.context.get_target()
        self.context.rectangle(area.x, area.y, area.width, area.height)
        self.context.clip()

        self.context.arc(self.x/2,self.y/2, self.diameter/2, 0, 2*math.pi)
        self.context.set_source_rgb(0, 0, 0)
        self.context.stroke()
        self.context.arc(self.x/2,self.y/2, self.diameter/2, 0, 2*math.pi)
        self.context.set_source_rgb(1, 1, 1)
        self.context.fill()

        self.context.arc(self.x/2,self.y/2, 5, 0, 2*math.pi)
        self.context.set_source_rgb(0, 0, 0)
        self.context.stroke()

        self.layout = self.context.create_layout()
        self.layout.set_font_description(pango.FontDescription("sans serif 8"))
        
        for circle in self.__circles:
            (x,y),diameter,color = circle
            self.context.arc(x,y,diameter,0,2*math.pi)
            self.context.set_source_rgba(color[0], color[1], color[2], .7)
            self.context.fill_preserve()
            self.context.set_source_rgb(0, 0, 0)
            self.context.set_line_width(2)
            self.context.stroke()
        
        for name in self.__names:
            (x,y), text, color,angle,size = name
            # If the text is reversed, we also change where we are placing it.
            extents = self.context.text_extents(text)
            ex = (extents[2]-extents[1])*math.cos(angle)
            ey = (extents[2]-extents[1])*math.sin(angle)
            x += math.cos(angle)*size
            y += math.sin(angle)*size
            # If the angle is between 90 and 270 degrees (on the left hand side),
            # we want to actually make it display downwards, somehow.
            if angle > math.pi/2.0 and angle <= math.pi:
                self.context.move_to(x+ex, y+ey)
            elif angle > math.pi and angle < math.pi + math.pi/2:
                self.context.move_to(x+ex, y+ey)
            else:
                self.context.move_to(x,y)
            self.context.set_source_rgb(color[0], color[1], color[2])
            self.context.update_layout(self.layout)
            matrix = self.context.get_matrix()
            # If the name falls between 90 and 270, we reverse the text.
            if angle > math.pi/2.0 and angle < math.pi/2.0 + math.pi:
                matrix.scale(-1, -1)
                self.context.set_matrix(matrix)
            self.context.rotate(angle)
            self.layout.set_text(text)
            self.context.show_layout(self.layout)
            self.context.rotate(-angle)
            # And we reverse it back to normal.
            if angle > math.pi/2.0 and angle < math.pi/2.0 + math.pi:
                matrix.scale(-1, -1)
                self.context.set_matrix(matrix)
        

    def resize_cb(self, widget, event):
        # Shape the window into just the circle it is drawing.
        x, y, width, height = widget.get_allocation()
        self.x, self.y = width, height
        self.diameter = min(self.x, self.y)-1
        if self.hash is not None:
            self.visualize(self.hash)
        self.queue_draw()

    def button_press_cb(self, widget, event):
        leniency = 5
        for x in xrange(len(self.__circles), 0, -1):
            (x,y), diameter, color = self.__circles[x-1]
            if abs(event.x - x) < diameter+leniency and abs(event.y - y) < diameter+leniency:
                for text in self.__names:
                    (tx,ty), text, color,angle,size = text
                    if tx == x and ty == y:
                        self.emit("blob-clicked", event, text)
                        return
