from DefComCryptography import Encrypt, Decrypt, SignWithPrivateKey, VerifiySignedWithPublicKey
from Crypto.PublicKey import RSA
from datetime import datetime
from message import Message
from base64 import b64encode, b64decode
from time import sleep
from threading import Thread

class Conversation:
    '''
    Represents a conversation between participants
    '''
    def __init__(self, c_id, enc_sym_key, manager):
        '''
        Constructor
        :param c_id: ID of the conversation (integer)
        :param manager: instance of the ChatManager class
        :return: None
        '''
        self.id = c_id  # ID of the conversation
        self.all_messages = []  # all retrieved messages of the conversation
        self.printed_messages = []
        self.last_processed_msg_id = 0  # ID of the last processed message
        from chat_manager import ChatManager
        assert isinstance(manager, ChatManager)
        self.manager = manager # chat manager for sending messages
        self.public_key = manager.public_key
        self.run_infinite_loop = True
        self.msg_process_loop = Thread(
            target=self.process_all_messages) # message processing loop
        self.msg_process_loop.start()
        self.msg_process_loop_started = True
        self.symKey = RSA.importKey(self.manager.private_key).decrypt(b64decode(enc_sym_key))

    def append_msg_to_process(self, msg_json):
        '''
        Append a message to the list of all retrieved messages

        :param msg_json: the message in JSON encoding
        :return:
        '''
        self.all_messages.append(msg_json)

    def append_msg_to_printed_msgs(self, msg):
        '''
        Append a message to the list of printed messages

        :param msg: an instance of the Message class
        :return:
        '''
        assert isinstance(msg, Message)
        self.printed_messages.append(msg)

    def exit(self):
        '''
        Called when the application exists, breaks the infinite loop of message processing

        :return:
        '''
        self.run_infinite_loop = False
        if self.msg_process_loop_started == True:
            self.msg_process_loop.join()

    def process_all_messages(self):
        '''
        An (almost) infinite loop, that iterates over all the messages received from the server
        and passes them for processing

        The loop is broken when the application is exiting
        :return:
        '''
        while self.run_infinite_loop:
            for i in range(0, len(self.all_messages)):
                current_msg = self.all_messages[i]
                msg_raw = ""
                msg_id = 0
                owner_str = ""
                try:
                    # Get raw data of the message from JSON document representing the message
                    msg_raw = b64decode(current_msg["content"])
                    # Base64 decode message
                    msg_id = int(current_msg["message_id"])
                    # Get the name of the user who sent the message
                    owner_str = current_msg["owner"]
                    timestamp = current_msg["timestamp"]
                    signature = current_msg["signature"]
                    public_key = current_msg["public_key"]


                except KeyError as e:
                    print ("Received JSON does not hold a message")
                    continue
                except ValueError as e:
                    print ("Message ID is not a valid number:", current_msg["message_id"])
                    continue
                if msg_id > self.last_processed_msg_id:
                    # If the message has not been processed before, process it
                    self.process_incoming_message(msg_raw=msg_raw,
                                                  msg_id=msg_id,
                                                  owner_str=owner_str, 
                                                  timestamp = timestamp,
                                                  signature = signature,
                                                  public_key = public_key)

                    # Update the ID of the last processed message to the current
                    self.last_processed_msg_id = msg_id
                sleep(0.01)

    def setup_conversation(self):
        '''
        Prepares the conversation for usage
        :return:
        '''
        # You can use this function to initiate your key exchange
        # Useful stuff that you may need:
        # - name of the current user: self.manager.user_name
        # - list of other users in the converstaion: list_of_users = self.manager.get_other_users()
        # You may need to send some init message from this point of your code
        # you can do that with self.process_outgoing_message("...") or whatever you may want to send here...

        # Since there is no crypto in the current version, no preparation is needed, so do nothing
        # replace this with anything needed for your key exchange 
        pass


    def process_incoming_message(self, msg_raw, msg_id, owner_str, timestamp, signature, public_key):
        '''
        Process incoming messages
        :param msg_raw: the raw message
        :param msg_id: ID of the message
        :param owner_str: user name of the user who posted the message
        :param user_name: name of the current user
        :param print_all: is the message part of the conversation history?
        :return: None
        '''
        if not VerifiySignedWithPublicKey(public_key, signature, str(timestamp) + str(owner_str) + str(b64encode(msg_raw))):
            print "the signature for this message did not verify correctly"
            return

        #timestamp?y
        decoded_msg = Decrypt(b64encode(msg_raw),self.symKey)

        # print message and add it to the list of printed messages
        self.print_message(
            msg_raw=decoded_msg,
            owner_str=owner_str
        )

    def process_outgoing_message(self, msg_raw, originates_from_console=False):
        '''
        Process an outgoing message before Base64 encoding

        :param msg_raw: raw message
        :return: message to be sent to the server
        '''

        #timestamp
        timestamp =  datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        #encrypt message
        encMessage = Encrypt(msg_raw, self.symKey)

        message = timestamp + self.manager.user_name + encMessage
        
        #sig
        signature = SignWithPrivateKey(self.manager.private_key, message)

        # if the message has been typed into the console, record it, so it is never printed again during chatting
        if originates_from_console == True:
            # message is already seen on the console
            m = Message(
                owner_name=self.manager.user_name,
                timestamp=timestamp,
                content=encMessage,
                signature=signature,
                public_key=self.public_key
            )
            self.printed_messages.append(m)

        # process outgoing message here
		# example is base64 encoding, extend this with any crypto processing of your protocol
        #encoded_msg = b64encode(msg_raw)

        # post the message to the conversation
        self.manager.post_message_to_conversation(timestamp,encMessage,signature,self.public_key)

    def print_message(self, msg_raw, owner_str):
        '''
        Prints the message if necessary

        :param msg_raw: the raw message
        :param owner_str: name of the user who posted the message
        :return: None
        '''
        # Create an object out of the message parts
        msg = Message(content=msg_raw,
                      owner_name=owner_str)
        # If it does not originate from the current user or it is part of conversation history, print it
        if msg not in self.printed_messages:
            print (msg)
            # Append it to the list of printed messages
            self.printed_messages.append(msg)

    def __str__(self):
        '''
        Called when the conversation is printed with the print or str() instructions
        :return: string
        '''
        for msg in self.printed_messages:
            print (msg)

    def get_id(self):
        '''
        Returns the ID of the conversation
        :return: string
        '''
        return self.id

    def get_last_message_id(self):
        '''
        Returns the ID of the most recent message
        :return: number
        '''
        return len(self.all_messages)