import numpy as np
import tensorflow as tf
import re
import time 


# importing the datasets
line=open('movie_lines.txt', encoding='utf-8', errors= 'ignore').read().split('\n')
conversation = open('movie_conversations.txt', encoding='utf-8', errors= 'ignore').read().split('\n')

#mapping of dataset to Dictionary line nad its id
id2line = {}
for lines in line:
    _lines = lines.split(' +++$+++ ')
    if len(_lines)==5:
        id2line[_lines[0]] = _lines[4]
        

#Creating a list of all the conversations
    conversations_ids=[]
    for conversations in conversation[:-1]:
        _conversations=conversations.split(' +++$+++ ')[-1][1:-1].replace("'", "").replace(" ", "")
        conversations_ids.append(_conversations.split(','))
    
    
#getting separately the question and answers
questions = []
answers = []
for conversation in conversations_ids:
    for i in range(len(conversation) -1):
        questions.append(id2line[conversation[i]])
        answers.append(id2line[conversation[i+1]])
        
# Doing a first cleaning of the texts:
def clean_text(text):
    text=text.lower()
    text=re.sub(r"i'm", "i am", text)
    text=re.sub(r"he's", "he is", text)
    text=re.sub(r"she's", "she is", text)
    text=re.sub(r"that's", "that is", text)
    text=re.sub(r"what's", "what is", text)
    text=re.sub(r"where's", "where is", text)
    text=re.sub(r"\'ll", " will", text)
    text=re.sub(r"\'ve", " have", text)
    text=re.sub(r"\'re", " are", text)
    text=re.sub(r"\'d", " would", text)
    text=re.sub(r"won't", "will not", text)
    text=re.sub(r"can't", "can not", text)
    text=re.sub(r"[-()\"#/;:{}+|.?,]", "", text)
    return text


# Cleaning the quistions
clean_questions=[]
for question in questions:
    clean_questions.append(clean_text(question))


# Cleaning the answers
clean_answers=[]
for answer in answers:
    clean_answers.append(clean_text(answer))


# Creating a dictinary that maps each word to its number of accurences
word2count={}
for question in clean_questions:
    for word in question.split():
        if word not in word2count:
            word2count[word]=1
        else:
            word2count[word] += 1
            
             
for answer in clean_answers:
    for word in answer.split():
        if word not in word2count:
            word2count[word]=1
        else:
            word2count[word] += 1
            
            
#Creating tow dictionaries that map the questions words and the answers worsds to a unique integer
threshold=20
questionswords2int={}
word_number=0
for word, count in word2count.items():
    if count >= threshold:
        questionswords2int[word]=word_number
        word_number +=1

answerswords2int={}
word_number=0
for word, count in word2count.items():
    if count >= threshold:
        answerswords2int[word]=word_number
        word_number +=1
        
        
# Adding the last tokens to these two dictinaries
tokens=['<PAD>', '<EOS>', '<OUT>', '<SOS>', ]
for token in tokens:
    questionswords2int[token]=len(questionswords2int) + 1
for token in tokens:
    answerswords2int[token]=len(answerswords2int) + 1
    

# Creating the inverse dictionary of the answersword2int dictionary
answersints2word = {w_i: w for w, w_i in answerswords2int.items()}


# Adding the End of string token to the end of every answer
for i in range(len(clean_answers)):  
    clean_answers[i] +=' <EOS>'
    

# Translating all the questins and the answeres int integers
# and Replacing all the words the were filtered out by <OUT>
questions_into_int=[]
for question in clean_questions:
    ints=[]
    for word in question.split():
        if word not in questionswords2int:
            ints.append(questionswords2int['<OUT>'])
        else:
            ints.append(questionswords2int[word])
    questions_into_int.append(ints)
answers_into_int=[]
for answer in clean_answers:
    ints=[]
    for word in answer.split():
        if word not in answerswords2int:
            ints.append(answerswords2int['<OUT>'])
        else:
            ints.append(answerswords2int[word])            
    answers_into_int.append(ints)



# Sorting quistions and answers by the length of questions
sorted_clean_questions = []
sorted_clean_answers = []
for length in range(1, 25 +1):
    for i in enumerate(questions_into_int):
        if len(i[1])==length:
            sorted_clean_questions.append(questions_into_int[i[0]])
            sorted_clean_answers.append(questions_into_int[i[0]])


# Creating placehoders for the inputs and the targets
def model_inputs():
    inputs = tf.placeholder(tf.int32, [None, None], name = 'intput')
    targets = tf.placeholder(tf.int32, [None, None], name = 'target')
    lr = tf.placeholder(tf.float32, name = 'learning_rate')
    keep_prob = tf.placeholder(tf.float32, name = 'keep_prob')
    
    return inputs, targets, lr, keep_prob
    
    
# Preprocessing the targets (Pattern of targets) for decoder 
def preprocess_targets(targets, word2int, batch_size):
    left_side = tf.fill([batch_size, 1], word2int['<SOS>'])
    right_side = tf.strided_slice(targets, [0, 0], [batch_size, -1], [1,1])
    preprocessed_targets = tf.concat([left_side, right_side], 1)
    return preprocessed_targets
    
# Creating the Encoder RNN layer
def encoder_rnn(rnn_inputs, rnn_size, num_layers, keep_prob, sequence_length):
    lstm = tf.contrib.rnn.BasicLSTMCell(rnn_size)
    lstm_dropout = tf.contrib.rnn.DropoutWrapper(lstm, input_keep_prob = keep_prob)
    encoder_cell = tf.contrib.rnn.MultiRNNCell([lstm_dropout]*num_layers)
    encoder_output, encoder_state = tf.nn.bidirectional_rnn(cell_fw = encoder_cell,
                                               cell_bw = encoder_cell,
                                               sequence_length = sequence_length,
                                               inputs = rnn_inputs,
                                               dtype = tf.float32)
    return encoder_state

# Decoding the training set
    
def decode_training_set(encoder_state, decoder_cell, decoder_embedded_input, sequence_length, decoding_scope, output_function, keep_prob, batch_size):
    attention_states = tf.zeros([batch_size, 1, decoder_cell.output_size])
    attention_keys, attention_values, attention_score_function, attention_construct_function = tf.contrib.seq2seq.prepare_attention(attention_states, attention_option = 'bahdanau', num_units = decoder_cell.output_size)
    training_decoder_function = tf.contrib.seq2seq.attention_decoder_fn_train(encoder_state[0],
                                                                              attention_keys,
                                                                              attention_values,
                                                                              attention_score_function,
                                                                              attention_construct_function,
                                                                              name = "attn_dec_train")
    decoder_output, decoder_final_state, decoder_final_context_state = tf.contrib.seq2seq.dynamic_rnn_decoder(decoder_cell,
                                                                                                              training_decoder_function,
                                                                                                              decoder_embedded_input,
                                                                                                              sequence_length,
                                                                                                              scope = decoding_scope)
    decoder_output_dropout = tf.nn.dropout(decoder_output, keep_prob)
    return output_function(decoder_output_dropout)

#Decoding the test/validation set

def decode_test_set(encoder_state, decoder_cell, decoder_embeddeding_matrix, sos_id, eos_id, maximum_length, num_words, sequence_length, decoding_scope, output_function, keep_prob, batch_size):
    attention_states = tf.zeros([batch_size, 1, decoder_cell.output_size])
    attention_keys, attention_values, attention_score_function, attention_construct_function = tf.contrib.seq2seq.prepare_attention(attention_states, attention_option = 'bahdanau', num_units = decoder_cell.output_size)
    test_decoder_function = tf.contrib.seq2seq.attention_decoder_fn_inference(output_function,
                                                                              encoder_state[0],
                                                                              attention_keys,
                                                                              attention_values,
                                                                              attention_score_function,
                                                                              attention_construct_function,
                                                                              decoder_embeddeding_matrix,
                                                                              sos_id, eos_id,
                                                                              maximum_length,
                                                                              num_words,
                                                                              name = "attn_dec_inf")
    test_predictions, decoder_final_state, decoder_final_context_state = tf.contrib.seq2seq.dynamic_rnn_decoder(decoder_cell,
                                                                                                                test_decoder_function,
                                                                                                                scope = decoding_scope)
    return test_predictions

# Creating the Decoding RNN
    
def decoder_rnn(decoder_embedded_input, decoder_embeddings_matrix, encoder_state, num_words, sequence_length, rnn_size, num_layers, word2int, keep_prob, batch_size):
    with tf.variable_scope("decoding") as decoding_scope:
        lstm = tf.contrib.rnn.BasicLSTMCell(rnn_size)
        lstm_dropout = tf.contrib.rnn.DropoutWrapper(lstm, input_keep_prob = keep_prob)
        decoder_cell = tf.contrib.rnn.MultiRNNCell([lstm_dropout] * num_layers)
        weights = tf.truncated_normal_initializer(stddev = 0.1)
        biases = tf.zeros_initializer()
        output_function = lambda x: tf.contrib.layers.fully_connected(x,
                                                                      num_words,
                                                                      None,
                                                                      scope = decoding_scope,
                                                                      weights_intitializer = weights,
                                                                      biases_initializer = biases)
        training_predictions = decode_training_set(encoder_state,
                                                   decoder_cell,
                                                   decoder_embedded_input,
                                                   sequence_length,
                                                   decoding_scope,
                                                   output_function,
                                                   keep_prob,
                                                   batch_size)
        decoding_scope.reuse_ariables()
        test_predictions = decode_test_set(encoder_state,
                                          decoder_cell,
                                          decoder_embeddeding_matrix,
                                          word2int['<SOS>'],
                                          word2int['<EOS>'],
                                          sequence_length - 1,
                                          num_words,
                                          decoding_scope,
                                          output_function,
                                          keep_prob,
                                          batch_size)
    return training_predictions, test_predictions

# Building sec2sec model
    
def seq2seq(inputs, targets, keep_prob, batch_size, sequence_length, answers_num_words, questions_num_words, encoder_embedding_size, decoder_embedding_size, rnn_size, num_layers, questionswords2int):
    encoder_embedded_input = tf.contrib.layers.embed_sequence(inputs,
                                                              answers_num_words + 1,
                                                              initializer = tf.random_uniform_initializer(0, 1))
    encoder_state = encoder_rnn(encoder_embedded_input, rnn_size, num_layers, keep_prob, sequence_length)
    preprocessed_targets = preprocess_targets(targets, questionswords2int, batch_size)
    decoder_embeddings_matrix = tf.Variable(tf.random_uniform([questions_num_words + 1, decoder_embedding_size], 0, 1))
    decoder_embedded_input = tf.nn.embedding_lookup(decoder_embeddings_matrix, preprocessed_targets)
    training_predictions, test_predictions = decoder_rnn(decoder_embedded_input,
                                                         decoder_embeddings_matrix,
                                                         encoder_state,
                                                         questions_num_words,
                                                         sequence_length,
                                                         rnn_size,
                                                         num_layers,
                                                         questionswords2int,
                                                         keep_prob,
                                                         batch_size)


# Training Part
# Setting the Hyperparaamters

epochs = 100
batch_size = 64
rnn_layers = 3
encoding_embedding_size = 512
decoding_embedding_size = 512
learning_rate_decay = 0.9
min_learning_rate = 0.0001
keep_probability = 0.5  

# defining a session

tf.reset_default_graph()
session = tf.InteractiveSession()

# Loading the model inputs
inputs, targets, lr, keep_prob = model_inputs()

# Setting the sequence lenhth

sequence_length = tf.placeholder_with_default(25, None, name = 'sequence_length')

# Getting the shape of the inputs tensor

input_shape = tf.shape(inputs)


























    
        
    

























    
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    
    
    
    