import sys
import math
import torch
import numpy as np
import torch.nn as nn
from util import *
import torch.nn.functional as F
from torch.autograd import Variable
from datetime import datetime
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence

def ortho_weight(ndim):
	"""
	Random orthogonal weights

	Used by norm_weights(below), in which case, we
	are ensuring that the rows are orthogonal
	(i.e W = U \Sigma V, U has the same
	# of rows, V has the same # of cols)
	"""
	W = np.random.randn(ndim, ndim)
	u, s, v = np.linalg.svd(W)
	return u.astype('float32')

def norm_weight(nin, nout=None, scale=0.01, ortho=True):
	"""
	Random weights drawn from a Gaussian
	"""
	if nout is None:
		nout = nin
	if nout == nin and ortho:
		W = ortho_weight(nin)
	else:
		W = scale * np.random.randn(nin, nout)
	return W.astype('float32')

def generate_mask_2(values, sent_sizes):
	mask_matrix = np.zeros((len(sent_sizes), max(sent_sizes), values.size(2)))
	for i in range(len(sent_sizes)):
		mask_matrix[i][:sent_sizes[i]][:]=1
	if torch.cuda.is_available():
		mask_matrix = torch.Tensor(mask_matrix).cuda()
	else:
		mask_matrix = torch.Tensor(mask_matrix)
	return values*Variable(mask_matrix)

def generate_mask(lsent_sizes, rsent_sizes):
	mask_matrix=np.zeros((len(lsent_sizes),max(lsent_sizes),max(rsent_sizes)))
	for i in range(len(lsent_sizes)):
		mask_matrix[i][:lsent_sizes[i]][:rsent_sizes[i]]=1
	if torch.cuda.is_available():
		mask_matrix = torch.Tensor(mask_matrix).cuda()
	else:
		mask_matrix = torch.Tensor(mask_matrix)
	return Variable(mask_matrix)

class LSTM_Cell(nn.Module):

	def __init__(self, cuda, in_dim, mem_dim):
		super(LSTM_Cell, self).__init__()
		self.cudaFlag = cuda
		self.in_dim = in_dim
		self.mem_dim = mem_dim

		def new_gate():
			h = nn.Linear(self.mem_dim, self.mem_dim, bias=False)
			h.weight.data.copy_(torch.from_numpy(ortho_weight(self.mem_dim)))
			return h

		def new_W():
			w = nn.Linear(self.in_dim, self.mem_dim)
			w.weight.data.copy_(torch.from_numpy(ortho_weight(self.mem_dim)))
			return w

		self.ih = new_gate()
		self.fh = new_gate()
		self.oh = new_gate()
		self.ch = new_gate()

		self.cx = new_W()
		self.ox = new_W()
		self.fx = new_W()
		self.ix = new_W()


	def forward(self, input, h, c):
		u = F.tanh(self.cx(input) + self.ch(h))
		i = F.sigmoid(self.ix(input) + self.ih(h))
		f = F.sigmoid(self.fx(input) + self.fh(h))
		c = i*u + f*c
		o = F.sigmoid(self.ox(input) + self.oh(h))
		h = o * F.tanh(c)
		return c, h

class LSTM(nn.Module):
	def __init__(self, cuda, in_dim, mem_dim):
		super(LSTM, self).__init__()
		self.cudaFlag = cuda
		self.in_dim = in_dim
		self.mem_dim = mem_dim

		self.TreeCell = LSTM_Cell(cuda, in_dim, mem_dim)
		self.output_module = None

	def forward(self, x, x_mask):
		"""
		:param x: #step x #sample x dim_emb
		:param x_mask: #step x #sample
		:param x_left_mask: #step x #sample x #step
		:param x_right_mask: #step x #sample x #step
		:return:
		"""
		h = Variable(torch.zeros(x.size(1), x.size(2)))
		c = Variable(torch.zeros(x.size(1), x.size(2)))
		if torch.cuda.is_available():
			h=h.cuda()
			c=c.cuda()
		all_hidden=[]
		for step in range(x.size(0)):
			input=x[step] # #sample x dim_emb
			step_c, step_h=self.TreeCell(input, h, c)
			h=x_mask[step][:,None] * step_h + (1. - x_mask[step])[:,None] * h
			c = x_mask[step][:, None] * step_c + (1. - x_mask[step])[:, None] * c
			all_hidden.append(torch.unsqueeze(h,0))
		return torch.cat(all_hidden,0)

class ESIM(nn.Module):
	"""
		Implementation of the multi feed forward network model described in
		the paper "A Decomposable Attention Model for Natural Language
		Inference" by Parikh et al., 2016.

		It applies feedforward MLPs to combinations of parts of the two sentences,
		without any recurrent structure.
	"""
	def __init__(self, num_units, num_classes, vocab_size, embedding_size, pretrained_emb,
				 training=True, project_input=True,
				 use_intra_attention=False, distance_biases=10, max_sentence_length=30):
		"""
		Create the model based on MLP networks.

		:param num_units: size of the networks
		:param num_classes: number of classes in the problem
		:param vocab_size: size of the vocabulary
		:param embedding_size: size of each word embedding
		:param use_intra_attention: whether to use intra-attention model
		:param training: whether to create training tensors (optimizer)
		:param project_input: whether to project input embeddings to a
			different dimensionality
		:param distance_biases: number of different distances with biases used
			in the intra-attention model
		"""
		super(ESIM, self).__init__()
		self.vocab_size=vocab_size
		self.num_units = num_units
		self.num_classes = num_classes
		self.project_input = project_input
		self.embedding_size=embedding_size
		self.distance_biases=distance_biases
		self.max_sentence_length=max_sentence_length
		self.pretrained_emb=pretrained_emb

		self.dropout = nn.Dropout(p=0.5)
		self.word_embedding=nn.Embedding(vocab_size,embedding_size)

		self.lstm_intra=LSTM(torch.cuda.is_available(),embedding_size, num_units)

		self.linear_layer_compare = nn.Sequential(nn.Linear(4*num_units*2, num_units), nn.ReLU(), nn.Dropout(p=0.5))
		#                                          nn.Dropout(p=0.2), nn.Linear(num_units, num_units), nn.ReLU())

		self.lstm_compare=LSTM(torch.cuda.is_available(), embedding_size, num_units)

		self.linear_layer_aggregate = nn.Sequential(nn.Dropout(p=0.5), nn.Linear(4*num_units*2, num_units), nn.ReLU(),
													nn.Dropout(p=0.5), nn.Linear(num_units, num_classes))

		self.init_weight()

	def ortho_weight(self):
		"""
		Random orthogonal weights
		Used by norm_weights(below), in which case, we
		are ensuring that the rows are orthogonal
		(i.e W = U \Sigma V, U has the same
		# of rows, V has the same # of cols)
		"""
		ndim=self.num_units
		W = np.random.randn(ndim, ndim)
		u, s, v = np.linalg.svd(W)
		return u.astype('float32')

	def initialize_lstm(self):
		if torch.cuda.is_available():
			init=torch.Tensor(np.concatenate([self.ortho_weight(),self.ortho_weight(),self.ortho_weight(),self.ortho_weight()], 0)).cuda()
		else:
			init = torch.Tensor(
				np.concatenate([self.ortho_weight(), self.ortho_weight(), self.ortho_weight(), self.ortho_weight()], 0))
		return init

	def init_weight(self):
		#nn.init.normal(self.linear_layer_project,mean=0,std=0.1)
		#print(self.linear_layer_attend[3])
		#self.linear_layer_attend[1].weight.data.normal_(0, 0.01)
		#self.linear_layer_attend[1].bias.data.fill_(0)
		#self.linear_layer_attend[4].weight.data.normal_(0, 0.01)
		#self.linear_layer_attend[4].bias.data.fill_(0)
		self.linear_layer_compare[0].weight.data.normal_(0, 0.01)
		self.linear_layer_compare[0].bias.data.fill_(0)
		#self.linear_layer_compare[4].weight.data.normal_(0, 0.01)
		#self.linear_layer_compare[4].bias.data.fill_(0)
		self.linear_layer_aggregate[1].weight.data.normal_(0, 0.01)
		self.linear_layer_aggregate[1].bias.data.fill_(0)
		self.linear_layer_aggregate[4].weight.data.normal_(0, 0.01)
		self.linear_layer_aggregate[4].bias.data.fill_(0)
		self.word_embedding.weight.data.copy_(torch.from_numpy(self.pretrained_emb))

	def attention_softmax3d(self,raw_attentions):
		reshaped_attentions = raw_attentions.view(-1, raw_attentions.size(2))
		out=nn.functional.softmax(reshaped_attentions, dim=1)
		return out.view(raw_attentions.size(0),raw_attentions.size(1),raw_attentions.size(2))

	def _transformation_input(self,embed_sent, x1_mask):
		embed_sent = self.word_embedding(embed_sent)
		embed_sent = self.dropout(embed_sent)
		hidden=self.lstm_intra(embed_sent, x1_mask)
		return hidden


	def aggregate(self,v1, v2):
		"""
		Aggregate the representations induced from both sentences and their
		representations

		:param v1: tensor with shape (batch, time_steps, num_units)
		:param v2: tensor with shape (batch, time_steps, num_units)
		:return: logits over classes, shape (batch, num_classes)
		"""
		v1_mean = torch.mean(v1, 0)
		v2_mean = torch.mean(v2, 0)
		v1_max, _ = torch.max(v1, 0)
		v2_max, _ = torch.max(v2, 0)
		out = self.linear_layer_aggregate(torch.cat((v1_mean, v1_max, v2_mean, v2_max), 1))

		#v1_sum=torch.sum(v1,1)
		#v2_sum=torch.sum(v2,1)
		#out=self.linear_layer_aggregate(torch.cat([v1_sum,v2_sum],1))

		return out

	def cosine_interaction(self, tensor1, tensor2):
		"""
		:param tensor1: #step1 * dim
		:param tensor2: #step2 * dim
		:return: #step1 * #step2
		"""
		simCube_0=tensor1[0].view(1,-1)
		simCube_1=tensor2[0].view(1,-1)
		for i in range(tensor1.size(0)):
			for j in range(tensor2.size(0)):
				if not(i==0 and j==0):
					simCube_0=torch.cat((simCube_0, tensor1[i].view(1,-1)))
					simCube_1=torch.cat((simCube_1, tensor2[j].view(1,-1)))
		simCube=F.cosine_similarity(simCube_0, simCube_1)
		return simCube.view(tensor1.size(0),tensor2.size(0))

	def forward_old(self, x1, x1_mask, x2, x2_mask):
		# idx = [i for i in range(embed_sent.size(1) - 1, -1, -1)]
		# if torch.cuda.is_available():
		#	idx = torch.cuda.LongTensor(idx)
		# else:
		#	idx = torch.LongTensor(idx)

		x1 = self.word_embedding(x1)
		x1 = self.dropout(x1)
		x2 = self.word_embedding(x2)
		x2 = self.dropout(x2)

		idx_1 = [i for i in range(x1.size(0) - 1, -1, -1)]
		if torch.cuda.is_available():
			idx_1 = Variable(torch.cuda.LongTensor(idx_1))
		else:
			idx_1 = Variable(torch.LongTensor(idx_1))
		x1_r=torch.index_select(x1,0,idx_1)
		x1_mask_r=torch.index_select(x1_mask,0,idx_1)
		idx_2=[i for i in range(x2.size(0) -1, -1, -1)]
		if torch.cuda.is_available():
			idx_2 = Variable(torch.cuda.LongTensor(idx_2))
		else:
			idx_2 = Variable(torch.LongTensor(idx_2))
		x2_r=torch.index_select(x2,0,idx_2)
		x2_mask_r=torch.index_select(x2_mask, 0, idx_2)

		proj1=self.lstm_intra(x1, x1_mask)
		proj1_r=self.lstm_intra(x1_r, x1_mask_r)
		proj2=self.lstm_intra(x2, x2_mask)
		proj2_r=self.lstm_intra(x2_r, x2_mask_r)

		ctx1=torch.cat((proj1, torch.index_select(proj1_r,0,idx_1)),2)
		ctx2=torch.cat((proj2, torch.index_select(proj2_r, 0, idx_2)),2)
		# ctx1: #step1 x #sample x #dimctx
		# ctx2: #step2 x #sample x #dimctx
		ctx1 = ctx1 * x1_mask[:, :, None]
		ctx2 = ctx2 * x2_mask[:, :, None]

		# weight_matrix: #sample x #step1 x #step2
		weight_matrix = torch.matmul(ctx1.permute(1, 0, 2), ctx2.permute(1, 2, 0))
		weight_matrix_1 = torch.exp(weight_matrix - weight_matrix.max(1, keepdim=True)[0]).permute(1, 2, 0)
		weight_matrix_2 = torch.exp(weight_matrix - weight_matrix.max(2, keepdim=True)[0]).permute(1, 2, 0)

		# weight_matrix_1: #step1 x #step2 x #sample
		weight_matrix_1 = weight_matrix_1 * x1_mask[:, None, :]
		weight_matrix_2 = weight_matrix_2 * x2_mask[None, :, :]

		alpha = weight_matrix_1 / weight_matrix_1.sum(0, keepdim=True)
		beta = weight_matrix_2 / weight_matrix_2.sum(1, keepdim=True)

		ctx2_ = (torch.unsqueeze(ctx1,1) * torch.unsqueeze(alpha,3)).sum(0)
		ctx1_ = (torch.unsqueeze(ctx2, 0) * torch.unsqueeze(beta,3)).sum(1)

		inp1 = torch.cat([ctx1, ctx1_, ctx1 * ctx1_, ctx1 - ctx1_], 2)
		inp2 = torch.cat([ctx2, ctx2_, ctx2 * ctx2_, ctx2 - ctx2_], 2)
		inp1=self.dropout(self.linear_layer_compare(inp1))
		inp2=self.dropout(self.linear_layer_compare(inp2))
		inp1_r=torch.index_select(inp1, 0, idx_1)
		inp2_r=torch.index_select(inp2, 0, idx_2)

		v1=self.lstm_compare(inp1, x1_mask)
		v2=self.lstm_compare(inp2, x2_mask)
		v1_r = self.lstm_compare(inp1_r, x1_mask)
		v2_r = self.lstm_compare(inp2_r, x2_mask)
		v1=torch.cat((v1, torch.index_select(v1_r, 0, idx_1)),2)
		v2=torch.cat((v2, torch.index_select(v2_r, 0, idx_2)),2)
		logits = self.aggregate(v1, v2)
		return logits

	def forward(self, x1, x1_mask, x2, x2_mask):
		# idx = [i for i in range(embed_sent.size(1) - 1, -1, -1)]
		# if torch.cuda.is_available():
		#	idx = torch.cuda.LongTensor(idx)
		# else:
		#	idx = torch.LongTensor(idx)

		x1 = self.word_embedding(x1)
		x1 = self.dropout(x1)
		x2 = self.word_embedding(x2)
		x2 = self.dropout(x2)

		idx_1 = [i for i in range(x1.size(0) - 1, -1, -1)]
		if torch.cuda.is_available():
			idx_1 = Variable(torch.cuda.LongTensor(idx_1))
		else:
			idx_1 = Variable(torch.LongTensor(idx_1))
		x1_r=torch.index_select(x1,0,idx_1)
		x1_mask_r=torch.index_select(x1_mask,0,idx_1)
		idx_2=[i for i in range(x2.size(0) -1, -1, -1)]
		if torch.cuda.is_available():
			idx_2 = Variable(torch.cuda.LongTensor(idx_2))
		else:
			idx_2 = Variable(torch.LongTensor(idx_2))
		x2_r=torch.index_select(x2,0,idx_2)
		x2_mask_r=torch.index_select(x2_mask, 0, idx_2)

		proj1=self.lstm_intra(x1, x1_mask)
		proj1_r=self.lstm_intra(x1_r, x1_mask_r)
		proj2=self.lstm_intra(x2, x2_mask)
		proj2_r=self.lstm_intra(x2_r, x2_mask_r)

		ctx1=torch.cat((proj1, torch.index_select(proj1_r,0,idx_1)),2)
		ctx2=torch.cat((proj2, torch.index_select(proj2_r, 0, idx_2)),2)
		# ctx1: #step1 x #sample x #dimctx
		# ctx2: #step2 x #sample x #dimctx
		ctx1 = ctx1 * x1_mask[:, :, None]
		ctx2 = ctx2 * x2_mask[:, :, None]

		# weight_matrix: #sample x #step1 x #step2
		weight_matrix = torch.matmul(ctx1.permute(1, 0, 2), ctx2.permute(1, 2, 0))
		weight_matrix_1 = torch.exp(weight_matrix - weight_matrix.max(1, keepdim=True)[0]).permute(1, 2, 0)
		weight_matrix_2 = torch.exp(weight_matrix - weight_matrix.max(2, keepdim=True)[0]).permute(1, 2, 0)

		# weight_matrix_1: #step1 x #step2 x #sample
		weight_matrix_1 = weight_matrix_1 * x1_mask[:, None, :]
		weight_matrix_2 = weight_matrix_2 * x2_mask[None, :, :]

		alpha = weight_matrix_1 / weight_matrix_1.sum(0, keepdim=True)
		beta = weight_matrix_2 / weight_matrix_2.sum(1, keepdim=True)

		self.alpha=alpha
		self.beta=beta

		ctx2_ = (torch.unsqueeze(ctx1,1) * torch.unsqueeze(alpha,3)).sum(0)
		ctx1_ = (torch.unsqueeze(ctx2, 0) * torch.unsqueeze(beta,3)).sum(1)

		# cosine distance and Euclidean distance
		'''
		tmp_result=[]
		for batch_i in range(ctx1.size(1)):
			tmp_result.append(torch.unsqueeze(self.cosine_interaction(ctx1[:,batch_i,:], ctx2[:,batch_i,:]), 0))
		weight_matrix=torch.cat(tmp_result)
		weight_matrix_1 = torch.exp(weight_matrix - weight_matrix.max(1, keepdim=True)[0]).permute(1, 2, 0)
		weight_matrix_2 = torch.exp(weight_matrix - weight_matrix.max(2, keepdim=True)[0]).permute(1, 2, 0)

		# weight_matrix_1: #step1 x #step2 x #sample
		weight_matrix_1 = weight_matrix_1 * x1_mask[:, None, :]
		weight_matrix_2 = weight_matrix_2 * x2_mask[None, :, :]

		alpha = weight_matrix_1 / weight_matrix_1.sum(0, keepdim=True)
		beta = weight_matrix_2 / weight_matrix_2.sum(1, keepdim=True)

		ctx2_cos_ = (torch.unsqueeze(ctx1, 1) * torch.unsqueeze(alpha, 3)).sum(0)
		ctx1_cos_ = (torch.unsqueeze(ctx2, 0) * torch.unsqueeze(beta, 3)).sum(1)
		'''

		inp1 = torch.cat([ctx1, ctx1_, ctx1 * ctx1_, ctx1 - ctx1_], 2)
		inp2 = torch.cat([ctx2, ctx2_, ctx2 * ctx2_, ctx2 - ctx2_], 2)
		#inp1 = torch.cat([ctx1, ctx1_, ctx1_cos_, ctx1 * ctx1_, ctx1 * ctx1_cos_, ctx1 - ctx1_, ctx1 - ctx1_cos_], 2)
		#inp2 = torch.cat([ctx2, ctx2_, ctx2_cos_, ctx2 * ctx2_, ctx2 * ctx2_cos_, ctx2 - ctx2_, ctx2 - ctx2_cos_], 2)
		inp1=self.dropout(self.linear_layer_compare(inp1))
		inp2=self.dropout(self.linear_layer_compare(inp2))
		inp1_r=torch.index_select(inp1, 0, idx_1)
		inp2_r=torch.index_select(inp2, 0, idx_2)

		v1=self.lstm_compare(inp1, x1_mask)
		v2=self.lstm_compare(inp2, x2_mask)
		v1_r = self.lstm_compare(inp1_r, x1_mask)
		v2_r = self.lstm_compare(inp2_r, x2_mask)
		v1=torch.cat((v1, torch.index_select(v1_r, 0, idx_1)),2)
		v2=torch.cat((v2, torch.index_select(v2_r, 0, idx_2)),2)
		logits = self.aggregate(v1, v2)
		return logits
