import tornado.ioloop
import tornado.web
import tornado.websocket
import json
import numpy

#
# setup NEURON
#

execfile('themodel.py')

#
# most of this stuff should not require changing between models; it should really
# be put in a helper file
#

apply_settings()

update_dt = 1
def run():
    h.finitialize(-68)
    while h.t + h.dt < h.tstop:
        h.continuerun(min(h.tstop, h.t + update_dt))
        update_graphs()
    h.continuerun(min(h.tstop, h.t + update_dt))
    update_graphs()

def get_pts_between(x, y, z, d, arc, lo, hi):
    left_x = numpy.interp(lo, arc, x, left=x[0], right=x[-1])
    left_y = numpy.interp(lo, arc, y, left=y[0], right=y[-1])
    left_z = numpy.interp(lo, arc, z, left=z[0], right=z[-1])
    left_d = numpy.interp(lo, arc, d, left=d[0], right=d[-1])
    right_x = numpy.interp(hi, arc, x, left=x[0], right=x[-1])
    right_y = numpy.interp(hi, arc, y, left=y[0], right=y[-1])
    right_z = numpy.interp(hi, arc, z, left=z[0], right=z[-1])
    right_d = numpy.interp(hi, arc, d, left=x[0], right=d[-1])
    in_between = [[x0, y0, z0, d0] for (x0, y0, z0, d0, a0) in zip(x, y, z, d, arc) if lo < a0 < hi]
    if len(in_between) == 0:
        # ensure there is at least one interior point
        in_between = [[(left_x + right_x) * 0.5, (left_y + right_y) * 0.5, (left_z + right_z) * 0.5, (left_d + right_d) * 0.5]]
    result = [[left_x, left_y, left_z, left_d]] + in_between + [[right_x, right_y, right_z, right_d]]
    round3(result)
    return result

def _morph():
    morph = []
    h.define_shape()
    for sec in h.allsec():
        n3d = int(h.n3d(sec=sec))
        x = [h.x3d(i, sec=sec) for i in xrange(n3d)]
        y = [h.y3d(i, sec=sec) for i in xrange(n3d)]
        z = [h.z3d(i, sec=sec) for i in xrange(n3d)]
        d = [h.diam3d(i, sec=sec) for i in xrange(n3d)]
        arc = [h.arc3d(i, sec=sec) for i in xrange(n3d)]
        length = sec.L
        half_dx = 0.5 / sec.nseg
        for seg in sec:
            morph.append(get_pts_between(x, y, z, d, arc, (seg.x - half_dx) * length, (seg.x + half_dx) * length))
    return morph

def round3(pt_list):
    n = 3
    for i, pt in enumerate(pt_list):
        x, y, z, d = tuple(pt)
        pt_list[i] = [round(x, n), round(y, n), round(z, n), round(d, n)]
        
def update_graphs():
    data = [[tval, vval] for tval, vval in zip(t, v)]
    # update the line graph
    send_message_to_all({'command': 'setgraph', 'data': data})
    # update the shape plot
    colors = []
    key = []
    for sec in h.allsec():
        colors.extend([seg.v for seg in sec])
        key.extend(['%s(%g).v = %g' % (sec.hname().split('.')[-1], seg.x, seg.v) for seg in sec])
    colors = values_to_colors(colors, -80, 25)
    send_message_to_all({'command': 'setshapeplot', 'data': colors, 'key': key})

def two_char_hex(n):
    s = hex(int(n))[-2 :]
    if s[0] == 'x':
        s = '0' + s[1]
    return s

def hex_rgb(r, g, b):
    """expects r, g, b between 0 and 1"""
    return '#' + two_char_hex(r * 255) + two_char_hex(g * 255) + two_char_hex(b * 255)

def values_to_colors(values, lo, hi):
    # force extrema
    values = [min(max(v, lo), hi) for v in values]

    non_nan = [v for v in values if not numpy.isnan(v)]
    if len(non_nan) == 0:
        return ['black'] * len(values)
    length = float(hi - lo)
    if lo == hi:
        return ['black' if numpy.isnan(v) else 'red' for v in values]
    else:
        values = numpy.array([(v - lo) / length for v in values])
        # gradient from blue (lo) to red (hi)
        return [(hex_rgb(v, 0, 1 - v) if not numpy.isnan(v) else 'black') for v in values]


#
# Server
#

port = 8000

clients = {}
client_count = 0

class IndexHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        print 'indexhandler::get'
        self.render('index.html')

class WebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self, *args):
        global client_count
        client_count += 1
        print 'websockethandler::open'
        self.id = client_count
        clients[self.id] = self
        self.send_message({'command': 'setfields', 'data': settings})
    
    def on_message(self, message):
        global settings, default_settings
        message = json.loads(message)
        command = message['command']
        if command == 'getmorphology':
            self.send_message({'command': 'setmorphology', 'morphology': _morph()})
        elif command == 'resetdefaults':
            settings = dict(default_settings)
            apply_settings()
            send_message_to_all({'command': 'setfields', 'data': settings})
        elif command == 'setfields':
            for name, val in message.iteritems():
                if name in settings:
                    settings[name] = val
            apply_settings()
            send_message_to_all({'command': 'setfields', 'data': settings})
        elif command == 'run':
            run()
        else:
            print 'message received'
            print message
    
    def on_close(self):
        print 'websockethandler::on_close'
        if self.id in clients:
            # remove client from the "list" of clients to notify on change
            del clients[self.id]
    
    def send_message(self, message):
        self.write_message(json.dumps(message))


def send_message_to_all(message):
    for client in clients.values():
        client.send_message(message)
        
app = tornado.web.Application([
    (r'/socket', WebSocketHandler),
    (r"/(.*)", tornado.web.StaticFileHandler, {"path":r"www/"})    
])

if __name__ == '__main__':
    app.listen(port)
    tornado.ioloop.IOLoop.instance().start()
