# -*- coding: utf-8 -*-

###################################################################
############## python server for javascript client ################
###################################################################

# json-encoded
# command_type: let the client know what action to take
# response_type: let the server know what response the client has sent

###################################################################
################ libraries from websocket_server ##################
###################################################################

from websocket_server import WebsocketServer
import random
import json
import csv

from copy import deepcopy

###################################################################
############################ globals ##############################
###################################################################

global_participant_data = {} #indexed by ID
# keys: client_info, partner, role, trial_list, shared_trial_number

unpaired_clients = []
# client IDs while they are in the waiting room to be paired

phase_sequence = ['Start', 'PairParticipants', "Interaction", 'End']

target_list = [['ankle','window'], ['princess', 'island'], ['belief', 'flower'], 
               ['liquor', 'century'], ['dollar', 'people'], ['stomach', 'bedroom'],
               ['doctor', 'temper'], ['river', 'valley'], ['apple', 'sunshine'],
               ['winner', 'presence'],['lawyer', 'banker'], ['reality', 'illusion'],
               ['argument', 'agreement'], ['apartment', 'furniture'], ['camera', 'president']]
## The frequency should be further decided: meaning pairs occur in different frequency, but the total occurrences of each meaning should be equal.

###################################################################
############################ utility ##############################
###################################################################

# for randomization
def shuffle(l):
    return random.sample(l, len(l))

# for converting message strings to JSON string
def send_message_by_id(client_id, message):
    client = global_participant_data[client_id]['client_info']
    server.send_message(client, json.dumps(message))

# for checking all clients are in the list_of_ids and are connected to server
# if so, clients will be in global_participant_data
def all_connected(list_of_ids):
    connected_status = [id in global_participant_data for id in list_of_ids]
    if sum(connected_status) == len(list_of_ids):
        return True 
    else:
        return False

# for notifying any client's dropping out
def notify_stranded(list_of_ids):
    for id in list_of_ids:
        if id in global_participant_data:
            send_message_by_id(id, {"command_type": "PartnerDropout"})

###################################################################
########### connecting, disconnecting, sending messages ###########
###################################################################

# connecting
def new_client(client, server):
    client_id = client['id']
    print("New client connected and was given id %id" % client_id)
    global_participant_data[client_id] = {'client_info': client}
    #in giving instructions to clients
    enter_phase(client_id, "Start")

# disconnecting
def client_left(client, server):
    client_id = client['id']
    print("Client(%d) disconnected" % client['id'])
    if client_id in unpaired_clients:
        unpaired_clients.remove(client_id)
    # leaving due to End
    if 'partner' in global_participant_data[client_id]:
        if global_participant_data[client_id]['phase'] != 'End':
            partner = global_participant_data[client_id]['partner']
            notify_stranded([partner])
    del global_participant_data[client_id]

# parsing the messages from clients in json dictionary
def message_received(client, server, message):
    print("Client(%d) said: %s" % (client['id'], message))
    # other possible responses
    response = json.loads(message)
    response_code = response['response_type']
    handle_client_response(client['id'], response_code, response)

###################################################################
############################ phases ###############################
###################################################################

# check the current phase and moves towards
def progress_phase(client_id):
    current_phase = global_participant_data[client_id]['phase']
    current_phase_i = phase_sequence.index(current_phase)
    next_phase_i = current_phase_i + 1
    next_phase = phase_sequence[next_phase_i]
    enter_phase(client_id, next_phase)

# trigger the next phase
def enter_phase(client_id, phase):
    #update the phase info
    global_participant_data[client_id]['phase'] = phase
    #start the experiment
    if phase == 'Start':
        progress_phase(client_id)

    elif phase == 'PairParticipants':
        #send message to those clients who are in the waiting room
        send_message_by_id(client_id, {'command_type': "WaitingRoom"})
        unpaired_clients.append(client_id)
        #if immediately paired, move on
        #if two unpaired clients, remove from the unpaired list, and pair them
        if (len(unpaired_clients)%2 == 0):
            unpaired_one = unpaired_clients[0]
            unpaired_two = unpaired_clients[1]
            unpaired_clients.remove(unpaired_one)
            unpaired_clients.remove(unpaired_two)
            #put them in global data
            global_participant_data[unpaired_one]['partner'] = unpaired_two
            global_participant_data[unpaired_two]['partner'] = unpaired_one
            #they share a target list, and move on to the next phase
            shuffled_targets = shuffle(target_list)
            for c in [unpaired_one, unpaired_two]:
                global_participant_data[c]['trial_list'] = shuffled_targets
                global_participant_data[c]['shared_trial_counter'] = 0
                progress_phase(c)

    # after paired, they move onto instruction trial
    elif phase == 'Interaction':
        print('initialising for interaction')
        send_instructions(client_id, phase)
    
    # when hit the end
    elif phase == 'End':
        send_message_by_id(client_id, {"command_type": "EndExperiment"})

###################################################################
########################## LOOOOOOOOOP ############################
###################################################################

############## while handling different responses #################
def handle_client_response(client_id, response_code, full_response):
    print('handle_client_response', client_id, response_code, full_response)
    #1. client_info
    if response_code == "CLIENT_INFO":
        global_participant_data[client_id]['participantID'] = full_response['client_info']
    #2. interaction_instruction_complete
    elif response_code == "INTERACTION_INSTRUCTIONS_COMPLETE":
        initiate_interaction(client_id)
    #3. response in two roles
    elif response_code == "RESPONSE" and full_response['role']=='Sender':
        handle_sender_response(client_id, full_response)
    elif response_code == "RESPONSE" and full_response['role']=='Receiver':
        handle_receiver_response(client_id, full_response)
    #4. finished_feedback: switch roles
    elif response_code == "FINISHED_FEEDBACK":
        swap_roles_and_progress(client_id)
    #5. nonresponsive_partner
    elif response_code == "NONRESPONSIVE_PARTNER":
        pass 

###################### trial progressions #########################
#1. after instruction
def initiate_interaction(client_id):
    print("in initiate_interaction",client_id)
    partner_id = global_participant_data[client_id]['partner']
    list_of_participants = [client_id, partner_id]
    # check if both are still connected
    if not(all_connected(list_of_participants)):
        notify_stranded(list_of_participants)
    else:
        send_message_by_id(client_id, {"command_type": "WaitForPartner"})
        partner_role = global_participant_data[partner_id]['role']
        #if ready to start the game
        if partner_role == 'ReadyToInteract':
            print('Starting interaction')
            #randomize sender and receiver
            for client, role in zip(list_of_participants, shuffle(["Sender", "Receiver"])):
                global_participant_data[client]['role'] = role
            start_interaction_trial(list_of_participants)
        else: global_participant_data[client_id]['role'] = 'ReadyToInteract'

#2. sending distinct instructions to sender and receiver
def start_interaction_trial(list_of_participants):
    #check if both/all are still connected; if not, notify
    if not(all_connected(list_of_participants)):
        notify_stranded(list_of_participants)
    else:
        #figure out who is the sender
        sender_id = [id for id in list_of_participants if global_participant_data[id]['role']=="Sender"][0]
        #retrieve their trial list and trial counter
        trial_counter = global_participant_data[sender_id]['shared_trial_counter']
        trial_list = global_participant_data[sender_id]['trial_list']
        ntrials = len(trial_list)
        #check if the sender has more trials to run - if not, move on
        if trial_counter > ntrials:
            for c in list_of_participants:
                progress_phase(c)
        else:
            receiver_id = global_participant_data[sender_id]['partner']
            receiver_participant_id = global_participant_data[receiver_id]['participantID']
            target = trial_list[trial_counter]
            for c in list_of_participants:
                this_role = global_participant_data[c]['role']
                if this_role == "Sender":
                    instruction_string = {"command_type": "Sender",
                                          "meaning_stimuli": target,
                                          "partner_id": receiver_participant_id}
                else:
                    instruction_string = {"command_type": "WaitForPartner"}
                send_message_by_id(c, instruction_string)

#3. on sender's response to server and server sending this response to receiver
def handle_sender_response(sender_id, sender_response):
    print('handle_sender_response', sender_response)
    receiver_id = global_participant_data[sender_id]['partner']
    if not(all_connected([receiver_id])):
        notify_stranded([sender_id])
    else:
        #look up the target in the sender trial list
        trial_counter = global_participant_data[sender_id]['shared_trial_counter']
        trial_list = global_participant_data[sender_id]['trial_list']
        target = trial_list[trial_counter]
        #the sender's response is the signal
        
        sender_participant_id = global_participant_data[sender_id]['participantID']
        send_message_by_id(sender_id, {"command_type": "WaitForPartner"})
        #server sends this response to receiver
        send_message_by_id(receiver_id,
                                       {"command_type": "Receiver",
                                        "sender_signal": sender_response['response'],
                                        "meaning_choices": target, ## see above - I retrieved this from the sender's trial list
                                        "partner_id": sender_participant_id})

#4. receiver's guess and feedback
def handle_receiver_response(receiver_id, receiver_response):
    print("in handle_receiver_response")
    sender_id = global_participant_data[receiver_id]['partner']
    if not(all_connected([sender_id, receiver_id])):
        notify_stranded([sender_id, receiver_id])
    else:
        #which meaning pair:
        trial_n = global_participant_data[sender_id]['shared_trial_counter']
        target = global_participant_data[sender_id]['trial_list'][trial_n]
        #sender's selected signal & receiver's meaning guess
        signal = receiver_response['sender_signal']
        guess = receiver_response['response']
        if target == guess:
            score = 1
        else:
            score = 0
        feedback = {"command_type": "Feedback", "score": score,
                    "target": target, "signal": signal, "guess": guess}
        for c in [sender_id, receiver_id]:
            send_message_by_id(c, feedback)

#5. done one trial and switch the role
def swap_roles_and_progress(client_id):
    print('swap roles',client_id)
    partner_id = global_participant_data[client_id]['partner']
    if not(all_connected([client_id,partner_id])):
        notify_stranded([client_id,partner_id])
    else:
        #increment global counter - both participants will do this independently when they reach this point
        global_participant_data[client_id]['shared_trial_counter'] += 1

        this_client_role = global_participant_data[client_id]['role']
        partner_role = global_participant_data[partner_id]['role']
        if partner_role=='WaitingToSwitch':
            if this_client_role=='Sender':
                global_participant_data[client_id]['role'] = "Receiver"
                global_participant_data[partner_id]['role'] = "Sender"
            else:
                global_participant_data[client_id]['role'] = "Sender"
                global_participant_data[partner_id]['role'] = "Receiver"
            #next trial
            start_interaction_trial([client_id,partner_id])
        #Otherwise your partner is not yet ready, so just flag up that you are
        else:
            global_participant_data[client_id]['role'] = "WaitingToSwitch"

###################################################################
################### Instruction between Blocks ####################
###################################################################

def send_instructions(client_id,phase):
    if phase=='Interaction':
        #set role
        global_participant_data[client_id]['role'] = "ReadingInstructions"
        send_message_by_id(client_id,{"command_type": "Instructions","instruction_type": "Interaction"})

###################################################################
######################## Start the Server #########################
###################################################################

PORT=9002 #will this run on port 9001 ?

#standard stuff here from the websocket_server code
print('starting up')
server = WebsocketServer(PORT,'0.0.0.0')
server.set_fn_new_client(new_client)
server.set_fn_client_left(client_left)
server.set_fn_message_received(message_received)
server.run_forever()