'''
TODO:
V Realtime interrupts of the visualization need to be propagated to the simulation.
V Change visualization to show in same browser window, not a pop-up.
V Reset
V Communication debugging interface -> visualization interface (for reset for example, or step back).
V State visualization in debugging interface (on hover?).
V Communication visualization interface -> debugging interface (for 'select an entity', for example).
V As fast as possible simulation.
V Visualization of "current small step" (1-8).
V Breakpoints.
V Inject events.
V God events.
V Fix bug: no visualization when small stepping.
V Highlight transitioning nodes.
V Display simulation time.
V Realtime scale.
V Termination condition.
V Handling of multiple connections for a single output port when a message is routed.
- Remove and add element/port/port connection. (? validity)
- Proper serialization of state: now I only serialize primitive values, and ignore composite ones. Needs to have a translation between Python and JS.
- Parameters for simulation initialization.
- Robustness.
- Each client has their own debugger?
- Integrate with new version of SCCD (see Joeri's mail).
'''

import socket, sys, json, threading, time, pickle, copy

sys.path.append("pypdevs")
sys.path.append("..")

import Tkinter as tk
from particle_interaction import *
import sccd
from python_runtime.statecharts_core import Event

HOST, PORT = "127.0.0.1", 9999
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))

'''
This function actually receives messages from the debugging interface (or instrumented visualizatin interface).
The communication protocol is very basic: all messages are terminated with a new line. This function makes sure
only 'full' messages are returned.
'''
end='\n'
def recv_end(the_socket):
    total_data = []
    data = ''
    while True:
        data = the_socket.recv(8192)
        if end in data:
            total_data.append(data[:data.find(end)])
            break
        total_data.append(data)
        if len(total_data) > 1:
            last_pair = total_data[-2] + total_data[-1]
            if end in last_pair:
                total_data[-2]=last_pair[:last_pair.find(end)]
                total_data.pop()
                break
    return ''.join(total_data)

'''
Send a message to the communication layer (which will then be routed to either the debugging interface or the
visualization server, based on the first entry in the sent array). Each message needs to end with a new line,
as the protocol specifies.
'''
def send_msg(msg):
    try:
        sock.send(msg + '\n')
    except socket.error:
        sys.exit(0)

'''
Handle output messages of the simulation on the pos, color, and time ports, respectively.
'''
def handle_output_pos(msgs):
    for msg_contents in msgs:
        send_msg(json.dumps(["VIS", "POS_OUT", msg_contents]))

def handle_output_color(msgs):
    for msg_contents in msgs:
        send_msg(json.dumps(["VIS", "COLOR_OUT", msg_contents]))

def handle_output_time(msgs):
    for msg_contents in msgs:
        send_msg(json.dumps(["VIS", "TIME_OUT", "%.2f" % (float(msg_contents / 30.0))]))

'''
Serializes a Python object to a form where it is ready for JSON serialization.
'''
def todict(obj, classkey=None):
    if isinstance(obj, dict):
        data = {}
        for (k, v) in obj.items():
            if v.__class__.__name__.endswith("State") or not hasattr(v, '__dict__'): # TODO: UGLY!!!
                data[k] = todict(v, classkey)
        return data
    elif hasattr(obj, "__iter__"):
        return [todict(v, classkey) for v in copy.copy(obj) if (v.__class__.__name__.endswith("State") or not hasattr(v, '__dict__'))] # TODO: UGLY!!!
    elif hasattr(obj, "__dict__"):
        data = dict([(key, todict(value, classkey)) 
            for key, value in copy.copy(obj.__dict__).iteritems() 
            if not callable(value) and not key.startswith('_') and (value.__class__.__name__.endswith("State") or not hasattr(value, '__dict__'))]) # TODO: UGLY!!!
        if classkey is not None and hasattr(obj, "__class__"):
            data[classkey] = obj.__class__.__name__
        return data
    else:
        return obj

'''
Blocks until a message is received on the specified port. Immediately sends that message (which needs to be
forwarded to the debugging interface).
'''
def poll_port(port):
    while 1:
        msg = port.fetch(-1)
        send_msg(json.dumps(["DBG", todict(msg)]))
        
if __name__ == '__main__':
    # Simulation parameters.
    resolution = (800, 600)
    framerate = 30
    spawn_chance = 0.2
    die_chance = 0.01

    # Instantiate model to simulate.
    model = Root(Canvas(resolution), framerate, spawn_chance, die_chance)
    
    # Instantiate and start our debugger (compiled Statecharts model).
    sim_controller = sccd.Controller(model)
    sim_controller.start()
    
    # Forward debugging messages to debugging server (in a separate thread).
    reply_port = sim_controller.addOutputListener('reply')
    thrd = threading.Thread(target=poll_port,args=[reply_port])
    thrd.daemon = True
    thrd.start()
    
    # Listen for simulation output on specified DEVS ports.
    sim_controller.addInput(Event("set_listen_ports", "request", ["POS_OUT", handle_output_pos]))
    sim_controller.addInput(Event("set_listen_ports", "request", ["COLOR_OUT", handle_output_color]))
    sim_controller.addInput(Event("set_listen_ports", "request", ["TIME_OUT", handle_output_time]))
    
    print 'Waiting for commands...'
    
    # Main loop: receive a message from the debugging interface (or the instrumented visualization server) and send it to the debugger.
    while 1:
        try:
            msg = recv_end(sock)
        except socket.error:
            break
        msg_head, msg_body = msg[:4], msg[4:]
        if msg_head == 'dbg_':
            decoded_event = json.loads(msg_body);
            parameters = decoded_event["parameters"]
            if decoded_event["name"] == "god_event":
                event = Event("god_event", "request", parameters)
            elif decoded_event["name"] == "inject":
                event = Event("inject", "request", parameters)
            elif decoded_event["name"] == "add_breakpoint":
                event = Event("add_breakpoint", "request", parameters[0])
            elif decoded_event["name"] == "toggle_breakpoint":
                event = Event("toggle_breakpoint", "request", parameters)
            elif decoded_event["name"] == "del_breakpoint":
                event = Event("del_breakpoint", "request", parameters)
            elif decoded_event["name"] == "reset":
                event = Event("reset", "request", parameters)
            elif parameters[0] == "realtime":
                event = Event(parameters[0], "request", [{"termination_condition": termination_condition, "realtime_scale": float(parameters[1]["realtime_scale"])}])
            else:
                event = Event(parameters[0], "request", [{"termination_condition": termination_condition}])
            sim_controller.addInput(event)
        elif msg_head == 'vis_':
            event = Event("realtime_interrupt", "request", [msg_body])
            sim_controller.addInput(event)