from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import numpy as np
import random
from six.moves import xrange  # pylint: disable=redefined-builtin

import tensorflow as tf
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import math_ops
from tensorflow.python.ops import nn_ops
from tensorflow.python.ops import rnn
from tensorflow.python.ops import variable_scope
from nn import linear
from functions import sequence_loss, flatten_query

class PoemModel(object):
    def __init__(self, hps, init_emb=None):
        # Create the model
        self.hps = hps
        self.enc_len = hps.bucket[0]
        self.dec_len = hps.bucket[1]
        self.mode = self.hps.mode
        self.keep_prob = tf.placeholder(tf.float32)
        self.global_step = tf.Variable(0, trainable=False)

        self.zeros = np.zeros((self.hps.batch_size, self.hps.his_mem_slots), dtype=np.float32)
        self.gama = tf.constant(20.0)

        self.learning_rate = tf.Variable(float(self.hps.learning_rate), trainable=False)
        self.learning_rate_decay_op = \
            self.learning_rate.assign(self.learning_rate * self.hps.decay_rate)

        self.b_size = self.hps.batch_size if self.mode == 'train' else 1

        # Random bias for memory writing
        init_val = []
        v = 1.0
        for i in xrange(0, self.hps.his_mem_slots):
            init_val.append(v)
            v /= 5.0

        bias_val = []
        for i in xrange(0, self.b_size):
            if self.b_size > 1:
                random.shuffle(init_val)
            bias_val.append(np.array(init_val+[0.0]))
        bias_val = np.array(bias_val)
        print ("Shape of random bias: %s" % (str(np.shape(bias_val)))) #[b_size,his_mem_slots+1]
        self.random_bias = tf.constant(bias_val, dtype=tf.float32)

        # The null slot
        null_mem = np.zeros([self.b_size, self.hps.his_mem_size], dtype=np.float32) - 1e-2
        self.null_mem = np.expand_dims(null_mem, 1) #[batch_size,1,his_mem_size] 注意区分np和tf的expand_dim

        # Build the graph
        self.__build_placeholders()

        # Build word embedding
        with tf.variable_scope('word_embedding'), tf.device('/cpu:0'):
            if init_emb is not None:
                print ("Initialize embedding with pre-trained word2vec.")
                initializer = tf.constant_initializer(init_emb)
            else:
                print ("Initialize embedding with normal distribution.")
                initializer = tf.truncated_normal_initializer(stddev=1e-4)
            word_emb = tf.get_variable('word_emb', [self.hps.vocab_size, self.hps.emb_size],
                dtype=tf.float32, initializer=initializer, trainable= True)

        self.emb_enc_inps = [ [tf.nn.embedding_lookup(word_emb, x) for x in enc_inp] for enc_inp in self.enc_inps] #[None,emb_size]
        self.emb_dec_inps = [ [tf.nn.embedding_lookup(word_emb, x) for x in dec_inp] for dec_inp in self.dec_inps] #[None,emb_size]
        self.emb_key_inps = [ [tf.nn.embedding_lookup(word_emb, x) for x in self.key_inps[i] ]  #[None,emb_size]
            for i in xrange(0, self.hps.key_slots)]

        # Build genre embedding
        with tf.variable_scope('ph_embedding'), tf.device('/cpu:0'):
            # NOTE: we set fixed 36 phonology categories
            ph_emb = tf.get_variable('ph_emb', [36, self.hps.ph_emb_size], dtype=tf.float32,
                initializer=tf.truncated_normal_initializer(stddev=1e-4))
        emb_ph_inps = [ [tf.nn.embedding_lookup(ph_emb, x) for x in ph_inp] for ph_inp in self.ph_inps] #[None,ph_emb_size]

        with tf.variable_scope('len_embedding'), tf.device('/cpu:0'):
            len_emb = tf.get_variable('len_emb', [self.dec_len+1, self.hps.len_emb_size], dtype=tf.float32,
                initializer=tf.truncated_normal_initializer(stddev=1e-4))
        emb_len_inps = [ [tf.nn.embedding_lookup(len_emb, x) for x in len_inp] for len_inp in self.len_inps] #[None,len_emb_size]

        # Concatenate phonology embedding and length embedding to form the genre embedding
        self.emb_genre = [[] for x in xrange(self.hps.sens_num)]
        for step in xrange(0, self.hps.sens_num):
            for i in xrange(self.dec_len+1):
                self.emb_genre[step].append(array_ops.concat([emb_ph_inps[step][i], emb_len_inps[step][i]], 1) )

        # Cells for the encoder
        enc_cell_fw  = tf.nn.rnn_cell.GRUCell(self.hps.hidden_size)
        enc_cell_bw = tf.nn.rnn_cell.GRUCell(self.hps.hidden_size)

        self.enc_cell_fw =  tf.nn.rnn_cell.DropoutWrapper(enc_cell_fw,
            output_keep_prob=self.keep_prob, input_keep_prob = self.keep_prob)
        self.enc_cell_bw = tf.nn.rnn_cell.DropoutWrapper(enc_cell_bw,
            output_keep_prob=self.keep_prob, input_keep_prob = self.keep_prob)

        if self.hps.mode == 'train':
            self.build_train_graph()
        else:
            self.build_gen_graph()

        # saver
        self.saver = tf.train.Saver(tf.global_variables() , write_version=tf.train.SaverDef.V1)

    def __build_placeholders(self):
        sens_num = self.hps.sens_num
        self.enc_inps = [[] for x in xrange(sens_num)]
        self.dec_inps = [[] for x in xrange(sens_num)]
        self.enc_mask  = [[] for x in xrange(sens_num)]
        self.ph_inps = [[] for x in xrange(sens_num)]
        self.len_inps = [[] for x in xrange(sens_num)]
        self.write_masks = [[] for x in xrange(sens_num)]

        for step in xrange(0, sens_num):
            for i in xrange(self.enc_len):
                self.enc_inps[step].append(tf.placeholder(tf.int32, shape=[None], name="enc{0}_{1}".format(step, i)))
                self.write_masks[step].append(tf.placeholder(tf.float32, shape=[None, 1], name="write_masks{0}_{1}".format(step, i)))
            for i in xrange(self.dec_len + 1):
                self.dec_inps[step].append(tf.placeholder(tf.int32, shape=[None],name="dec{0}_{1}".format(step, i)))
                self.len_inps[step].append(tf.placeholder(tf.int32, shape=[None],name="len_inps{0}_{1}".format(step, i)))
                self.ph_inps[step].append(tf.placeholder(tf.int32, shape=[None],name="ph_inps{0}_{1}".format(step, i)))

            self.enc_mask[step] = tf.placeholder(tf.float32, shape=[None, self.enc_len, 1], name="enc_mask{0}".format(step))

        self.key_inps = [[] for x in xrange(self.hps.key_slots)]
        for i in xrange(self.hps.key_slots):
            # NOTE: We set that each keyword must consist of no more than 2 characters
            for j in xrange(2):
                self.key_inps[i].append(tf.placeholder(tf.int32, shape=[None], name="key{0}_{1}".format(i, j)))
        self.key_mask = tf.placeholder(tf.float32, shape=[None, self.hps.key_slots, 1], name="key_mask")

        self.trg_weights = [[] for x in xrange(sens_num)]
        for step in xrange(0, sens_num):
            for i in xrange(self.dec_len + 1):
                self.trg_weights[step].append(tf.placeholder(tf.float32, shape=[None], name="weight{0}_{1}".format(step, i)))

        self.targets = [[] for x in xrange(sens_num)]
        for step in xrange(0, sens_num):
            self.targets[step] = [self.dec_inps[step][i + 1] for i in xrange(len(self.dec_inps[step]) - 1)]

        # For beam search
        self.beam_key_align = tf.placeholder(tf.float32,
            shape=[None, self.hps.key_slots], name="beam_key_align")
        self.beam_attn_states = tf.placeholder(tf.float32,
            shape=[None, self.enc_len, self.hps.hidden_size*2], name="beam_attn_states")
        self.beam_initial_state = tf.placeholder(tf.float32,
            shape=[None, self.hps.hidden_size] , name="beam_initial_state")
        self.beam_key_states = tf.placeholder(tf.float32,
            shape=[None, self.hps.key_slots, self.hps.hidden_size*2], name="beam_key_states")
        self.beam_global_trace = tf.placeholder(tf.float32,
            shape=[None, self.hps.global_trace_size], name="beam_global_trace")

        self.beam_his_mem = tf.placeholder(tf.float32,
            [None, self.hps.his_mem_slots, self.hps.his_mem_size], name="beam_his_mem")
        self.beam_his_mem_mask = tf.placeholder(tf.float32,
            [None, self.hps.his_mem_slots], name="beam_his_mem_mask")
        self.beam_topic_trace = tf.placeholder(tf.float32,
            [None, self.hps.topic_trace_size + self.hps.key_slots], name="beam_topic_trace")

        # For history memory write
        self.beam_mem_states = []
        for i in xrange(self.dec_len):
            self.beam_mem_states.append(tf.placeholder(tf.float32,
                shape=[None, self.hps.hidden_size*2],name="beam_mem_states{0}".format(i)))

    def build_gen_graph(self):
        print ("using device: %s" % self.hps.device)
        with tf.device(self.hps.device), tf.variable_scope("graph"):
            self.key_initial_state, self.key_states = self.__build_key_memory()
            self.enc_state, self.attn_states, _  = self.__build_encoder(0)

            enc_mask_unpack =  tf.unstack(self.enc_mask[0], axis = 2)[0] #长度为1的list，[batch_size,enc_len]
            key_mask_unpack =  tf.unstack(self.key_mask, axis = 2)[0] #长度为1的list，[batch_size,key_slots]
            total_mask = array_ops.concat([self.beam_his_mem_mask, key_mask_unpack, enc_mask_unpack], 1) #[batch_size,his_mem_slots+key_slots+enc_len]
            total_attn_states = array_ops.concat([self.beam_his_mem, self.beam_key_states, self.beam_attn_states], 1)
                #[batch_size, his_mem_slots, his_mem_size] [batch_size, key_slots, hidden_size*2] [batch_size, enc_len, hidden_size*2]
            # normed_dec_outs, dec_outs, dec_states, attn_weights
            self.next_out, _, self.next_state, self.next_align = self.__build_decoder([self.emb_dec_inps[0][0]],
                total_attn_states, total_mask, self.beam_global_trace, self.beam_initial_state, [self.emb_genre[0][0]], self.beam_topic_trace)

            self.new_topic_trace = self.__topic_trace_update(self.beam_topic_trace, self.beam_key_align, self.beam_key_states)
            self.new_global_trace = self.__global_trace_update(self.beam_global_trace, self.beam_attn_states)
            self.new_his_mem =  self.__write_memory(self.beam_his_mem,
                self.beam_mem_states, self.beam_global_trace, 0)

    def build_train_graph(self):
        print ("using device: %s" % self.hps.device)
        with tf.device(self.hps.device):

            normed_outs_vec, loss_vec = self.__build_graph()

            params = tf.trainable_variables()

            regularizers = []
            #print(len(params))
            for param in params:
                #name = param.name
                #print (name)
                regularizers.append(tf.nn.l2_loss(param))

            regular_loss = math_ops.reduce_sum(regularizers)
            gen_loss = math_ops.reduce_mean(loss_vec)
            total_loss = gen_loss + 1e-5 * regular_loss
            opt = tf.train.AdamOptimizer(learning_rate=self.learning_rate)
            gradients = tf.gradients(total_loss, params)
            clipped_gradients, norm = tf.clip_by_global_norm(gradients, self.hps.max_gradient_norm)

            self.update = opt.apply_gradients( zip(clipped_gradients, params), global_step=self.global_step)
            self.gradients = clipped_gradients

            # fetch
            self.gen_loss = gen_loss
            self.outputs = normed_outs_vec #长度为sens_num的list，每个元素是长度为dec_len的list，每个元素为[batch_size,vocab_size]

    def __build_graph(self):
        with tf.variable_scope("graph"):
            key_initial_state, key_states = self.__build_key_memory()
            global_trace = array_ops.zeros([self.hps.batch_size,
                self.hps.global_trace_size], dtype=tf.float32)

            his_mem = array_ops.zeros([self.hps.batch_size, self.hps.his_mem_slots,
                self.hps.his_mem_size], dtype=tf.float32)

            topic_trace = array_ops.zeros([self.hps.batch_size,
                self.hps.topic_trace_size+self.hps.key_slots], dtype=tf.float32)

            his_mem_mask = tf.constant(self.zeros)

            normed_outs_vec, loss_vec = [], []

            for step in xrange(0, self.hps.sens_num):
                if step > 0:
                        key_initial_state = None
                        variable_scope.get_variable_scope().reuse_variables()
                normed_outs, loss, global_trace, his_mem, topic_trace, \
                attn_weights = self.__build_seq2seq(global_trace, key_initial_state,
                    key_states, his_mem, his_mem_mask, topic_trace, step)

                if step >=1:
                    his_mem_sum = math_ops.reduce_sum(tf.abs(his_mem), axis=2) #[batch_size,his_mem_slots]
                    his_mem_bool = tf.not_equal(his_mem_sum, 0)
                    his_mem_mask = tf.to_float(his_mem_bool)

                normed_outs_vec.append(normed_outs) #长度为sens_num的list，每个元素是长度为dec_len的list，每个元素为[batch_size,vocab_size]
                loss_vec.append(tf.identity(loss)) #长度为sens_num的list，每个元素是一个数
            return normed_outs_vec, loss_vec

    def __build_seq2seq(self, global_trace, key_initial_state, key_states,
        his_mem, his_mem_mask, topic_trace, step):
        enc_state, attn_states, enc_outs = self.__build_encoder(step)
        if not (key_initial_state is None):
            initial_state = key_initial_state #[batch_size,hidden_size]
        else:
            initial_state = enc_state

        enc_mask_unpack =  tf.unstack(self.enc_mask[step], axis = 2)[0] #共有1个元素的list，每个元素是[batch_size,self.enc_len]
        key_mask_unpack =  tf.unstack(self.key_mask, axis = 2)[0] #共有1个元素的list,每个元素是[batch_size,self.hps.key_slots]
        total_mask = array_ops.concat( [his_mem_mask, key_mask_unpack, enc_mask_unpack], 1) #his_mem_mask [batch_size, self.hps.his_mem_slots]的0矩阵，常数，不是placeholder
        total_attn_states = array_ops.concat( [his_mem, key_states, attn_states], 1) #total_mask为[batch_size,enc_len+key_slots+his_mem_slots]
            #total_attn_states [batch_size,his_mem_slots+key_slots+enc_len,hidden_size*2]
        normed_dec_outs, dec_outs, dec_states, attn_weights = self.__build_decoder(
            self.emb_dec_inps[step][ : self.dec_len], total_attn_states, total_mask,
            global_trace, initial_state, self.emb_genre[step], topic_trace)

        concat_aligns = array_ops.concat(attn_weights, 0) #attn_weights是长度为dec_len的list，每个元素是list，该list只有一个元素,tensor [batch_size,attn_length]
        key_align = concat_aligns[:, :, self.hps.his_mem_slots:self.hps.his_mem_slots+self.hps.key_slots] #concat_aligns tensor [dec_len,batch_size,attn_length]
        key_align = math_ops.reduce_mean(key_align, axis=0) #key_align [dec_len,batch_size,key_slots] 变为[batch_size,key_slots]
        new_topic_trace = self.__topic_trace_update(topic_trace, key_align, key_states)

        new_his_mem = self.__write_memory(his_mem, enc_outs, global_trace, step)
        new_global_trace = self.__global_trace_update(global_trace, attn_states)
        loss = sequence_loss(dec_outs, self.targets[step][: self.dec_len], self.trg_weights[step][ : self.dec_len]) #dec_len的list，每个元素[batch_size,vocab_size] dec_len的list，每个元素[batch_size] dec_len的list，每个元素[batch_size]
        return normed_dec_outs, loss, new_global_trace, new_his_mem, new_topic_trace, attn_weights
        #normed_dec_outs是长度为dec_len的list，每个元素[batch_size,vocab_size]（经过了softmax）
    def __build_key_memory(self):
        #print ("memory")
        key_states = []
        with variable_scope.variable_scope("EncoderRNN"):
            for i in xrange(0, self.hps.key_slots):
                if i > 0:
                    variable_scope.get_variable_scope().reuse_variables() #重用原来的变量，而不是新创建
                (outputs , state_fw, state_bw)  = rnn.static_bidirectional_rnn(
                    self.enc_cell_fw, self.enc_cell_bw, self.emb_key_inps[i], dtype=tf.float32) #emb_key_inps是长度为step的list，每个元素都是[batch_size,dim]的tensor
                key_state = array_ops.concat([state_fw, state_bw], 1) #tensor state_fw和state_bw的shape:[batch_size,hidden_size]
                key_states.append(key_state) #tensor key_state的shape为[batch_size,2*hidden_size]

        with variable_scope.variable_scope("key_memory"):
            key_states = [array_ops.reshape(e, [-1, 1, self.enc_cell_fw.output_size*2]) for e in key_states]
            key_states = array_ops.concat(key_states, 1) #返回tensor [-1,key_slots,self.enc_cell_fw.output_size*2]
            key_states = tf.multiply(self.key_mask, key_states) #element-wise,支持广播

            final_state = math_ops.reduce_mean(key_states, axis=1) #tensor [-1,self.enc_cell_fw.output_size*2]
            final_state = linear(final_state, self.hps.hidden_size, True,  scope="key_initial") #[batch_size,hidden_size]
            final_state = tf.tanh(final_state)

        return final_state, key_states

    def __build_encoder(self, step):

        with variable_scope.variable_scope("EncoderRNN", reuse=True): #为什么reuse???
            (outputs , enc_state_fw, enc_state_bw)  = rnn.static_bidirectional_rnn(
                    self.enc_cell_fw, self.enc_cell_bw, self.emb_enc_inps[step][:self.enc_len], dtype=tf.float32) #input是长度为bucket[0]的list,每个元素都是[batch_size,dim]的tensor

            enc_outs = outputs #长度为time的list，每个元素为[batch,cell_fw.output_size + cell_bw.output_size]

        with variable_scope.variable_scope("seq2seq_Encoder"):
            enc_state =  enc_state_bw #反向
            final_state = linear(enc_state, self.hps.hidden_size, True,  scope="enc_initial")
            final_state = tf.tanh(final_state)

            top_states = [array_ops.reshape(e, [-1, 1, self.enc_cell_fw.output_size*2]) for e in enc_outs]
            attention_states = array_ops.concat(top_states, 1) #[batch_size,enc_len,self.enc_cell_fw.output_size*2]

            final_attn_states = tf.multiply(self.enc_mask[step], attention_states) #enc_mask的shape是[batch_size,self.enc_len,1]

        return final_state, final_attn_states, enc_outs

    def __build_decoder(self, dec_inps, total_attn_states, total_mask, global_trace,
        initial_state, genre, topic_trace):
        with variable_scope.variable_scope("seq2seq_Decoder"):

            dec_cell = tf.nn.rnn_cell.GRUCell(self.hps.hidden_size)
            dec_cell = tf.nn.rnn_cell.DropoutWrapper(dec_cell, output_keep_prob=self.keep_prob,
                input_keep_prob = self.keep_prob)

            output_size = self.hps.vocab_size # num_decoder_symbols is vocabulary size
            if not total_attn_states.get_shape()[1:3].is_fully_defined():
                raise ValueError("Shape[1] and [2] of attn_states must be known: %s" % total_attn_states.get_shape())

            dec_outs, dec_states, attn_weights  = [], [], []
            normed_dec_outs = []

            # build attn_mask
            imask = 1.0 - total_mask
            attn_mask = tf.where(tf.equal(imask, 1), tf.ones_like(imask) * (-float('inf')), imask) #float('inf') 正无穷
            #true的地方不变，false的地方变成后面的

            calcu_attention = lambda query : self.__attention_calcu(total_attn_states, query, attn_mask, "calcu_attention")

            state = initial_state
            decoder_input_size = initial_state.get_shape()[1].value #hidden_size
            for i, inp in enumerate(dec_inps): #某个句子的第几个字
                if i > 0:
                    variable_scope.get_variable_scope().reuse_variables()
                input_size = inp.get_shape().with_rank(2)[1]
                if input_size.value is None:
                    raise ValueError("Could not infer input size from input: %s" % inp.name)

                # Read memory
                attns, align = calcu_attention([flatten_query(state), global_trace, topic_trace])

                with variable_scope.variable_scope("input_merge"):
                    x = linear([inp, attns, genre[i], global_trace], decoder_input_size, True) #[batch_size,hidden_size]

                # Run the GRU
                cell_out, state = dec_cell(x, state) #参数(inputs,state) [batch_size,input_size] [batch_size,state_size]
                #返回(output,new_state)
                with variable_scope.variable_scope("OutputProjection"):
                    output = linear(cell_out, output_size, True)

                dec_outs.append(tf.identity(output))
                normed_dec_outs.append(tf.identity(tf.nn.softmax(output)))
                attn_weights.append([align])
                dec_states.append(tf.identity(state))

            return  normed_dec_outs, dec_outs, dec_states, attn_weights

    def __attention_calcu(self, attentions, query, attn_mask, scope):
        with variable_scope.variable_scope(scope):
            attn_length = attentions.get_shape()[1].value  # the length of a input sentence
            attn_size = attentions.get_shape()[2].value  # hidden state size of encoder, that is 2*size
            # To calculate W1 * h_t we use a 1-by-1 convolution, need to reshape before.
            # Remember we use bidirectional RNN
            hidden = array_ops.reshape(attentions, [-1, attn_length, 1, attn_size])
            # Size of query vectors for attention
            # query vector is decoder state
            attention_vec_size = attn_size

            k = variable_scope.get_variable("AttnW", [1, 1, attn_size, attention_vec_size])
            hidden_features = nn_ops.conv2d(hidden, k, [1, 1, 1, 1], "SAME") #input [batch, in_height, in_width, in_channels]
            v = variable_scope.get_variable("AttnV", [attention_vec_size])  #filter [filter_height, filter_width, in_channels, out_channels]
                #hidden_features [batch_size,attn_length,1,attn_size]
            y = linear(query, attention_vec_size, True)
            y = array_ops.reshape(y, [-1, 1, 1, attention_vec_size])
            # Attention mask is a softmax of v^T * tanh(...).
            s = math_ops.reduce_sum( v * math_ops.tanh(hidden_features + y), [2, 3]) # * multiply 广播
            a = nn_ops.softmax(s + attn_mask) #s [batch_size,attn_length]
            # Now calculate the attention-weighted vector d.
            d = math_ops.reduce_sum( array_ops.reshape(a, [-1, attn_length, 1, 1]) * hidden, [1, 2]) #[batch_size,attn_size]
            d = array_ops.reshape(d, [-1, attn_size])  #remember this size
            return d, a

    # Write history memory
    def __write_memory(self, his_mem, enc_states, global_trace, step):
        with variable_scope.variable_scope("write_memory"):
            mem_slots = his_mem.get_shape()[1].value
            mem_size = his_mem.get_shape()[2].value

            for i, state in enumerate(enc_states):
                if i > 0:
                    variable_scope.get_variable_scope().reuse_variables()

                # Concatenate history memory with the null slot
                tmp_mem = array_ops.concat([his_mem, tf.identity(self.null_mem)], axis=1) #[batch_size,his_mem_slots+1,his_mem_size]

                hidden = array_ops.reshape(tmp_mem, [-1, mem_slots+1, 1, mem_size])
                k = variable_scope.get_variable("AttnW", [1, 1, mem_size, mem_size])
                mem_features = nn_ops.conv2d(hidden, k, [1, 1, 1, 1], "SAME")
                v = variable_scope.get_variable("AttnV", [mem_size])

                mstate = state
                y = linear([flatten_query(mstate), global_trace], mem_size, True, scope = "query_trans")
                y = array_ops.reshape(y, [-1, 1, 1, mem_size])
                s = math_ops.reduce_sum(v * math_ops.tanh(mem_features + y), [2, 3])  #[batch_size,mem_slots+1]
                random_mask = 1.0 - tf.sign(math_ops.reduce_sum(tf.abs(tmp_mem), axis=2)) #[batch_size,his_mem_slots+1]
                    #tf.sign(x) 若x==0，返回0；若x<0，返回-1；若x>0，返回1
                # The random_mask shows if a slot is empty, 1 empty, 0 not empty.
                # The null mask is 1 if there is at least 1 empty slot.
                null_mask = random_mask[:, 0:self.hps.his_mem_slots]
                null_mask = math_ops.reduce_sum(null_mask, axis=1) #[batch_size]
                null_mask = tf.sign(null_mask)

                bias = self.random_bias * random_mask #random_bias tensor [batch_size,his_mem_slots+1]
                max_bias = tf.reduce_max(bias, axis=1) #[batch_size]
                max_bias = tf.expand_dims(max_bias, axis=1) #[batch_size,1]
                bias = tf.divide(bias, max_bias + 1e-12)

                max_s = tf.expand_dims(math_ops.reduce_max(s, axis=1), axis=1) #[batch_size,1]

                thred1 = tf.ones([self.b_size, self.hps.his_mem_slots+1], dtype=tf.float32)
                thred2 = tf.zeros([self.b_size, self.hps.his_mem_slots+1], dtype=tf.float32)
                thred = tf.where(tf.equal(null_mask, 1), thred1, thred2)

                bias1 = bias * tf.abs(max_s) * thred
                s1 = s + bias1 #为什么要加bias???
                a = nn_ops.softmax(s1) #[batch_size,his_mem_slots+1]

                max_val = tf.reduce_max(a, axis=1) #[batch_size]
                max_val = tf.expand_dims(max_val, axis=1) #[batch_size,1]

                if self.mode == 'train':
                    float_mask0 = tf.tanh(self.gama * (a - max_val)) + 1.0
                elif self.mode == 'decode':
                    float_mask0 = tf.sign(a - max_val) + 1.0

                mask = self.write_masks[step][i]
                float_mask = tf.multiply(mask, float_mask0)
                float_mask = tf.expand_dims(float_mask, axis=2) #[batch_size,his_mem_slots+1,1]
                #print (np.shape(float_mask))

                w_states = tf.tile(mstate, [1, mem_slots]) #[batch_size,2*hidden_size] 变为[batch_size,mem_slots*2*hidden_size]
                w_states = array_ops.reshape(w_states, [-1, mem_slots, mem_size])

                final_mask = float_mask[:, 0:self.hps.his_mem_slots, :] #[batch_size,his_mem_slots,1]
                #print (final_mask.get_shape())
                his_mem = (1.0 - final_mask) * his_mem + final_mask * w_states

            return his_mem

    def __topic_trace_update(self, topic_trace, key_align, key_states): #topic_trace [batch_size,topic_trace_size+key_slots] key_align [batch_size,key_slots] key_states [batch_size,key_slots,hidden_size*2]
        with variable_scope.variable_scope("topic_trace_update"):
            key_used = math_ops.reduce_mean(tf.multiply(key_states,
                tf.expand_dims(key_align, axis=2)), axis=1) #[batch_size,key_slots,1] => [batch_size,key_slots,2*hidden_size] => [batch_size,2*hidden_size]
            new_topic_trace = linear([topic_trace[:, 0:self.hps.topic_trace_size], key_used],
                self.hps.topic_trace_size, True)
            new_topic_trace = tf.tanh(new_topic_trace) #[batch_size,topic_trace_size]
            attn_los = topic_trace[:, self.hps.topic_trace_size:] + key_align #[batch_size,key_slots]

            fin_topic_trace = array_ops.concat([new_topic_trace, attn_los], 1)
            return fin_topic_trace

    def __global_trace_update(self, global_trace, enc_states): #enc_states [batch_size,time,self.enc_cell_fw.output_size*2]
        with variable_scope.variable_scope("global_trace_update", reuse=None):
            state = math_ops.reduce_mean(enc_states, axis=1) #[batch_size,self.enc_cell_fw.output_size*2]
            new_global_trace = math_ops.tanh(linear([global_trace, state],
                self.hps.global_trace_size, True)) #[batch_size,global_trace_size]
            new_global_trace = array_ops.reshape(new_global_trace, [-1, self.hps.global_trace_size])
            return new_global_trace

    #_____________________________________________
    # Functions for training or generation

    def step(self, session, data_dic, forward_only):
        '''For training one batch'''
        keep_prob = 1.0 if forward_only else 0.75

        input_feed = {}
        input_feed[self.keep_prob] = keep_prob

        for step in xrange(self.hps.key_slots):
            # NOTE: each topic word must consist of no more than 2 characters
            for j in xrange(2):
                input_feed[self.key_inps[step][j].name] = data_dic['key_inps'][step][j]
        input_feed[self.key_mask.name] = data_dic['key_mask']

        for step in xrange(0, self.hps.sens_num):
            if len(data_dic['enc_inps'][step]) != self.enc_len:
                raise ValueError("length must be equal %d != %d." %
                    (len(data_dic['enc_inps'][step]), self.enc_len))

            if len(data_dic['dec_inps'][step]) != self.dec_len:
                raise ValueError("length must be equal %d != %d. " %
                 (len(data_dic['dec_inps'][step]), self.dec_len))

            if len(data_dic['trg_weights'][step]) != self.dec_len:
                raise ValueError(" length must be equal %d != %d." %
                    (len(data_dic['trg_weights'][step]), self.dec_len))

            for l in xrange(self.enc_len):
                input_feed[self.enc_inps[step][l].name] = data_dic['enc_inps'][step][l]
                input_feed[self.write_masks[step][l].name] = data_dic['write_masks'][step][l]
            for l in xrange(self.dec_len):
                input_feed[self.dec_inps[step][l].name] = data_dic['dec_inps'][step][l]
                input_feed[self.trg_weights[step][l].name] = data_dic['trg_weights'][step][l]
                input_feed[self.len_inps[step][l].name] = data_dic['len_inps'][step][l]
                input_feed[self.ph_inps[step][l].name] = data_dic['ph_inps'][step][l]

            last_target = self.dec_inps[step][self.dec_len].name
            input_feed[last_target] = np.ones([self.hps.batch_size], dtype=np.int32) * self.hps.PAD_ID
            input_feed[self.enc_mask[step].name] =data_dic['enc_mask'][step]

        output_feed = []
        for step in xrange(0, self.hps.sens_num):
            for l in xrange(self.dec_len):  # Output logits.
                output_feed.append(self.outputs[step][l]) #[batch_size,vocab_size]

        if not forward_only:
            output_feed += [self.update, self.gen_loss]
        else:
            output_feed += [self.gen_loss]  # Loss for this batch.

        outputs = session.run(output_feed, input_feed)

        logits = [] #长度为4的list，每个元素是长度为dec_len的list，每个元素[batch_size,vocab_size]
        for step in xrange(0, self.hps.sens_num):
            logits.append(outputs[step*self.dec_len:(step+1)*self.dec_len])

        n = self.dec_len * self.hps.sens_num
        if not forward_only:
            return logits, outputs[n+1]
        else:
            return logits, outputs[n]

    #----------------------------------------
    # Some apis for beam search
    def key_memory_computer(self, session, key_inps, key_mask):
        input_feed = {}
        input_feed[self.keep_prob] = 1.0
        for step in xrange(self.hps.key_slots):
            for j in xrange(2):
                input_feed[self.key_inps[step][j].name] = key_inps[step][j]
        input_feed[self.key_mask.name] = key_mask

        output_feed = [self.key_initial_state, self.key_states]
        outputs = session.run(output_feed, input_feed)
        return outputs[0], outputs[1]  # key_initial_state, key_states

    def encoder_computer(self, session, enc_inps, enc_mask):
        assert self.enc_len == len(enc_inps)
        input_feed = {}
        input_feed[self.keep_prob] = 1.0
        for l in xrange(self.enc_len):
            input_feed[self.enc_inps[0][l].name] = enc_inps[l]

        input_feed[self.enc_mask[0].name] = enc_mask
        output_feed = [self.enc_state, self.attn_states]
        outputs = session.run(output_feed, input_feed)
        return outputs[0], outputs[1]  # encoder_state, attention_states

    def decoder_state_computer(self, sess, dec_inp, len_inp, ph_inp, prev_state,
        attn_states, key_states, his_mem, global_trace, enc_mask, key_mask,
         his_mem_mask, topic_trace):
        input_feed = {}
        input_feed[self.keep_prob] = 1.0

        input_feed[self.dec_inps[0][0].name] = dec_inp #为什么是第0个？？？
        input_feed[self.len_inps[0][0].name] = len_inp
        input_feed[self.ph_inps[0][0].name] = ph_inp

        input_feed[self.beam_attn_states.name] = attn_states
        input_feed[self.enc_mask[0].name] = enc_mask
        input_feed[self.beam_initial_state.name] = prev_state
        input_feed[self.beam_key_states.name] = key_states
        input_feed[self.key_mask.name] = key_mask
        input_feed[self.beam_global_trace.name] = global_trace
        input_feed[self.beam_topic_trace.name] = topic_trace

        input_feed[self.beam_his_mem.name] = his_mem
        input_feed[self.beam_his_mem_mask.name] = his_mem_mask

        output_feed = [self.next_out[0], self.next_state[0], self.next_align[0][0]] #为什么！！！

        outputs = sess.run(output_feed, input_feed)

        return outputs[0], outputs[1], outputs[2]# next_output, next_state, next_align

    def topic_trace_computer(self, session, key_states, prev_topic_trace, key_align):
        input_feed = {}
        input_feed[self.keep_prob] = 1.0
        input_feed[self.beam_key_states.name] = key_states
        input_feed[self.beam_topic_trace.name] = prev_topic_trace
        input_feed[self.beam_key_align.name] = key_align
        output_feed = [self.new_topic_trace]

        outputs = session.run(output_feed, input_feed)
        return outputs[0]

    def global_trace_computer(self, session, prev_global_trace, prev_attn_states):
        input_feed = {}
        input_feed[self.keep_prob] = 1.0
        input_feed [self.beam_global_trace] = prev_global_trace
        input_feed[self.beam_attn_states] = prev_attn_states
        output_feed = [self.new_global_trace]

        outputs = session.run(output_feed, input_feed)

        return outputs[0]

    def his_mem_computer(self, session, his_mem, prev_enc_states, mem_write_mask, global_trace):
        input_feed = {}
        input_feed[self.keep_prob] = 1.0
        input_feed[self.beam_his_mem.name] = his_mem

        for l in xrange(self.enc_len):
            input_feed[self.beam_mem_states[l]] = prev_enc_states[l]
            input_feed[self.write_masks[0][l]] = mem_write_mask[l]
        input_feed [self.beam_global_trace.name] = global_trace

        output_feed = [self.new_his_mem]
        outputs = session.run(output_feed, input_feed)

        return outputs[0]


"""
Preliminary checks before the code is run.

Ensures that the correct folders exists that contain the data and that config exists etc.
"""
import os
import json
import numpy as np

from classes.Dict import AttrDict
from definitions import ROOT_PATH, DATASET_PATH, OUTPUT_PATH
from utility.file_util import create_dir, file_exist
from utility.logger import get_logger

# Makes sure the default config and online label config files exist.
def check_config_files():
    default_config_path = os.path.join(ROOT_PATH, 'json_configs', 'default.json')
    label_config_path = os.path.join(ROOT_PATH, 'json_configs', 'file_management.json')

    try:
        with open(default_config_path, 'r') as default_c, open(label_config_path) as label_c:
            return AttrDict(json.load(default_c)), AttrDict(json.load(label_c))
    except OSError as e:
        get_logger().error(f'Config file missing at: {e.filename}')

# Data for 10 subjects should exist - creates folders for each subject if none yet exist.
def check_data_folders():
    valid_subjects = list(range(10))

    for subject in valid_subjects:
        create_dir(os.path.join(DATASET_PATH, f'subject_{subject}'), recursive=True)
        create_dir(os.path.join(DATASET_PATH, f'subject_{subject}', 'training'))
        create_dir(os.path.join(DATASET_PATH, f'subject_{subject}', 'online_test'))
        create_dir(os.path.join(DATASET_PATH, f'subject_{subject}', 'dwell_tuning'))

    create_dir(os.path.join(OUTPUT_PATH, 'results'))
    create_dir(os.path.join(OUTPUT_PATH, 'data'))
    create_dir(os.path.join(OUTPUT_PATH, 'features'))

# Checks if the online_test data label files specified in the file_management.json config files exist.
def check_for_label_files(label_config):
    for k, v in label_config.items():
        path = os.path.join(DATASET_PATH, k, 'online_test')
        try:
            if not np.all(list(map(os.path.exists, [os.path.join(path, i) for i in v['label_files']]))):
                raise FileNotFoundError(f'Label file missing at {k}')
        except FileNotFoundError as e:
            get_logger().error(e)


import numpy as np
from flask import Flask, render_template, request, redirect, url_for, flash, send_file
#from flask_sqlalchemy import SQLAlchemy
import pickle
from nltk import wordpunct_tokenize
from nltk.corpus import stopwords
from textblob import TextBlob
from babel.numbers import format_currency
import pandas as pd

# A dictionary if language codes with ISO 639-1 encoding and their respective languages
languages = {'aa': 'Afar', 'ab': 'Abkhazian', 'ae': 'Avestan', 'af': 'Afrikaans', 'ak': 'Akan', 'am': 'Amharic', 'an': 'Aragonese',
 'ar': 'Arabic', 'as': 'Assamese', 'av': 'Avaric', 'ay': 'Aymara', 'az': 'Azerbaijani', 'ba': 'Bashkir', 'be': 'Belarusian',
 'bg': 'Bulgarian', 'bh': 'Bihari languages', 'bi': 'Bislama', 'bm': 'Bambara', 'bn': 'Bengali', 'bo': 'Tibetan', 'br': 'Breton',
 'bs': 'Bosnian', 'ca': 'Catalan; Valencian', 'ce': 'Chechen', 'ch': 'Chamorro', 'co': 'Corsican', 'cr': 'Cree', 'cs': 'Czech',
 'cu': 'Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic', 'cv': 'Chuvash', 'cy': 'Welsh',
 'da': 'Danish', 'de': 'German', 'dv': 'Divehi; Dhivehi; Maldivian', 'dz': 'Dzongkha', 'ee': 'Ewe', 'el': 'Greek, Modern (1453-)',
 'en': 'English', 'eo': 'Esperanto', 'es': 'Spanish; Castilian', 'et': 'Estonian', 'eu': 'Basque', 'fa': 'Persian', 'ff': 'Fulah',
 'fi': 'Finnish', 'fj': 'Fijian', 'fo': 'Faroese', 'fr': 'French', 'fy': 'Western Frisian', 'ga': 'Irish', 'gd': 'Gaelic; Scottish Gaelic',
 'gl': 'Galician', 'gn': 'Guarani', 'gu': 'Gujarati', 'gv': 'Manx', 'ha': 'Hausa', 'he': 'Hebrew', 'hi': 'Hindi', 'ho': 'Hiri Motu',
 'hr': 'Croatian', 'ht': 'Haitian; Haitian Creole', 'hu': 'Hungarian', 'hy': 'Armenian', 'hz': 'Herero',
 'ia': 'Interlingua (International Auxiliary Language Association)', 'id': 'Indonesian', 'ie': 'Interlingue; Occidental', 'ig': 'Igbo',
 'ii': 'Sichuan Yi; Nuosu', 'ik': 'Inupiaq', 'io': 'Ido', 'is': 'Icelandic', 'it': 'Italian', 'iu': 'Inuktitut', 'ja': 'Japanese',
 'jv': 'Javanese', 'ka': 'Georgian', 'kg': 'Kongo', 'ki': 'Kikuyu; Gikuyu', 'kj': 'Kuanyama; Kwanyama', 'kk': 'Kazakh',
 'kl': 'Kalaallisut; Greenlandic', 'km': 'Central Khmer', 'kn': 'Kannada', 'ko': 'Korean', 'kr': 'Kanuri', 'ks': 'Kashmiri',
 'ku': 'Kurdish', 'kv': 'Komi', 'kw': 'Cornish', 'ky': 'Kirghiz; Kyrgyz', 'la': 'Latin', 'lb': 'Luxembourgish; Letzeburgesch',
 'lg': 'Ganda', 'li': 'Limburgan; Limburger; Limburgish', 'ln': 'Lingala', 'lo': 'Lao', 'lt': 'Lithuanian', 'lu': 'Luba-Katanga',
 'lv': 'Latvian', 'mg': 'Malagasy', 'mh': 'Marshallese', 'mi': 'Maori', 'mk': 'Macedonian', 'ml': 'Malayalam', 'mn': 'Mongolian',
 'mr': 'Marathi', 'ms': 'Malay', 'mt': 'Maltese', 'my': 'Burmese', 'na': 'Nauru', 'nb': 'Bokmål, Norwegian; Norwegian Bokmål',
 'nd': 'Ndebele, North; North Ndebele', 'ne': 'Nepali', 'ng': 'Ndonga', 'nl': 'Dutch; Flemish', 'nn': 'Norwegian Nynorsk; Nynorsk, Norwegian',
 'no': 'Norwegian', 'nr': 'Ndebele, South; South Ndebele', 'nv': 'Navajo; Navaho', 'ny': 'Chichewa; Chewa; Nyanja',
 'oc': 'Occitan (post 1500)', 'oj': 'Ojibwa', 'om': 'Oromo', 'or': 'Oriya', 'os': 'Ossetian; Ossetic', 'pa': 'Panjabi; Punjabi',
 'pi': 'Pali', 'pl': 'Polish', 'ps': 'Pushto; Pashto', 'pt': 'Portuguese', 'qu': 'Quechua', 'rm': 'Romansh', 'rn': 'Rundi',
 'ro': 'Romanian; Moldavian; Moldovan', 'ru': 'Russian', 'rw': 'Kinyarwanda', 'sa': 'Sanskrit', 'sc': 'Sardinian', 'sd': 'Sindhi',
 'se': 'Northern Sami', 'sg': 'Sango', 'si': 'Sinhala; Sinhalese', 'sk': 'Slovak', 'sl': 'Slovenian', 'sm': 'Samoan', 'sn': 'Shona',
 'so': 'Somali', 'sq': 'Albanian', 'sr': 'Serbian', 'ss': 'Swati', 'st': 'Sotho, Southern', 'su': 'Sundanese', 'sv': 'Swedish',
 'sw': 'Swahili', 'ta': 'Tamil', 'te': 'Telugu', 'tg': 'Tajik', 'th': 'Thai', 'ti': 'Tigrinya', 'tk': 'Turkmen', 'tl': 'Tagalog',
 'tn': 'Tswana', 'to': 'Tonga (Tonga Islands)', 'tr': 'Turkish', 'ts': 'Tsonga', 'tt': 'Tatar', 'tw': 'Twi', 'ty': 'Tahitian',
 'ug': 'Uighur; Uyghur', 'uk': 'Ukrainian', 'ur': 'Urdu', 'uz': 'Uzbek', 've': 'Venda', 'vi': 'Vietnamese', 'vo': 'Volapük',
 'wa': 'Walloon', 'wo': 'Wolof', 'xh': 'Xhosa', 'yi': 'Yiddish', 'yo': 'Yoruba', 'za': 'Zhuang; Chuang', 'zh': 'Chinese', 'zu': 'Zulu'}

app = Flask(__name__)
model = pickle.load(open('iris_model.pkl', 'rb'))

@app.route('/')
def home():
    return render_template('index.html')

@app.route('/aboutus')
def aboutus_page():
    return render_template('aboutus.html')

@app.route('/iris')
def iris_page():
    return render_template('iris.html')

@app.route('/language')
def language_page():
    return render_template('language.html')

@app.route('/sentiment')
def sentiment_page():
    return render_template('sentiment.html')

@app.route('/rubik')
def rubiks_page():
    return render_template('rubik.html')

@app.route('/stamp_collection')
def stamps_page():
    return render_template('stamp.html')

@app.route('/loan_calculator')
def loan_page():
    return render_template('loan.html')

@app.route('/iris/predict',methods=['POST'])
def predict_iris():
    #For rendering results on HTML GUI
    int_features = [float(x) for x in request.form.values()]
    final_features = [np.array(int_features)]
    print(final_features)
    prediction = model.predict(final_features)
    output = prediction[0]
    return render_template('iris.html', prediction_text='Iris species is : {}'.format(output))

@app.route('/language/detect',methods=['POST'])
def detect_language():
    languages_ratios = {}
    text = str([x for x in request.form.values()])
    tokens = wordpunct_tokenize(text)
    words = [word.lower() for word in tokens]
    for language in stopwords.fileids():
        stopwords_set = set(stopwords.words(language))
        words_set = set(words)
        common_elements = words_set.intersection(stopwords_set)
        languages_ratios[language] = len(common_elements)
    most_rated_language = max(languages_ratios, key=languages_ratios.get).capitalize()
    return render_template('language.html', detection_text='The detected language is : {}'.format(most_rated_language))

@app.route('/sentiment/predict',methods=['POST'])
def predict_sentiment():
    text = str([x for x in request.form.values()])
    text_sentiment_score = TextBlob(text).sentiment.polarity
    if text_sentiment_score >= 0.05:
        text_sentiment = "Positive"
    elif text_sentiment_score <= -0.05:
        text_sentiment = "Negative"
    else:
        text_sentiment = "Neutral"
    return render_template('sentiment.html', sent_prediction_score_text='The sentiment score is : {}'.format(text_sentiment_score), sent_prediction_text='The sentiment is : {}'.format(text_sentiment))

@app.route('/loan_calculator/result',methods=['POST'])
def loan_result():
    int_features = [x for x in request.form.values()]
    interest_rate_selection = int_features.pop(2)
    int_features = [float(i) for i in int_features]
    final_features = [np.array(int_features)]
    p = float(final_features[0][0])
    r = float(final_features[0][1])
    n = int(final_features[0][2])
    if interest_rate_selection == 'permonth':
        temp = 'Per Month'
        var = (1 + (r/100)) ** n
        emi = round((p * (r/100) * ((var)/(var - 1))), 2)
        A = round((emi * n), 2)
        I = round((A - p), 2)
        p_i = []
        I_i = []
        month = []
        for i in range(1, n+1):
            month.append(i)
            p_i.append(format_currency(round((((1 / (1 + (r/100))) ** (n - i + 1)) * emi), 2), 'INR', locale='en_IN'))
            I_i.append(format_currency(round((emi - (((1 / (1 + (r/100))) ** (n - i + 1)) * emi)), 2), 'INR', locale='en_IN'))
        df = pd.DataFrame(list(zip(month, p_i, I_i)), columns=['Month', 'Principal', 'Interest'])
        df['EMI'] = format_currency(round(emi, 2), 'INR', locale='en_IN')
        A_new = format_currency(A, 'INR', locale='en_IN')
        I_new = format_currency(I, 'INR', locale='en_IN')
        emi_new = format_currency(emi, 'INR', locale='en_IN')
        p_new = format_currency(p, 'INR', locale='en_IN')
    else:
        temp = 'Per Annum'
        var = (1 + (r/1200)) ** n
        emi = round((p * (r/1200) * ((var)/(var - 1))), 2)
        A = round((emi * n), 2)
        I = round((A - p), 2)
        p_i = []
        I_i = []
        month = []
        for i in range(1, n+1):
            month.append(i)
            p_i.append(format_currency(round((((1 / (1 + (r/1200))) ** (n - i + 1)) * emi), 2), 'INR', locale='en_IN'))
            I_i.append(format_currency(round((emi - (((1 / (1 + (r/1200))) ** (n - i + 1)) * emi)), 2), 'INR', locale='en_IN'))
        df = pd.DataFrame(list(zip(month, p_i, I_i)), columns=['Month', 'Principal', 'Interest'])
        df['EMI'] = format_currency(round(emi, 2), 'INR', locale='en_IN')
        A_new = format_currency(A, 'INR', locale='en_IN')
        I_new = format_currency(I, 'INR', locale='en_IN')
        emi_new = format_currency(emi, 'INR', locale='en_IN')
        p_new = format_currency(p, 'INR', locale='en_IN')
    return render_template('loan.html', principal_value='{}'.format(p_new), principal_text='Loan Amount: \u20B9 {}'.format(int(p)), interest_rate_text='Rate of Interest: {}%'.format(r), interest_rate_value='{}%'.format(r), interest_rate_selection_value='{}'.format(temp), tenure_text='Tenure: {} months'.format(int(n)), tenure_value='{} months'.format(int(n)), total_amount_text='Total Amount payable: \u20B9 {}'.format(A), total_amount_value='{}'.format(A_new), total_interest_text='Total Interest payable: \u20B9 {}'.format(I), total_inrest_value='{}'.format(I_new), emi_text='EMI: \u20B9 {}'.format(emi), emi_value='{}'.format(emi_new), tables=[df.to_html(classes='data table table-hover table-bordered text-center w-auto table-center', table_id='emi_table', index=False)], titles=df.columns.values)

#@app.route('/loan_calculator/result/download', methods = ['GET'])
#def download_excel():
#    return send_file('EMI_calculations.xlsx')

if __name__ == "__main__":
    app.run(debug=True)


import xlwt
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render
from rest_framework import status, viewsets, generics
from rest_framework.authtoken.models import Token
from rest_framework.decorators import api_view, permission_classes
from rest_framework.views import APIView
from rest_framework.response import Response
from .serializers import AssessmentResultsSerializer, ResultSerializer, CA_ItemSerializer, LecturerCourseSerializer, AssessmentSerializer
from django.views.decorators.csrf import csrf_exempt
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework import authentication
from rest_framework.authtoken.models import Token
from .models import *

class ResultsViewset(viewsets.ModelViewSet):
    queryset = Result.objects.all()
    serializer_class = ResultSerializer
    permission_classes = [IsAuthenticated]

class CA_ItemsViewset(viewsets.ReadOnlyModelViewSet):
    queryset = CA_Item.objects.all()
    serializer_class = CA_ItemSerializer
    permission_classes = [IsAuthenticated]

def get_user_from_token(token):
    user = Token.objects.get(pk=token).user
    print("username: {}".format(user))
    return user

def get_lecturer_from_user(user):
    lecturer = Lecturer.objects.get(lecturer=user)
    print("lecturer role: {}".format(lecturer.role))
    return lecturer

def get_lecturer_name(lecturer):
    lecturer_first_name = lecturer.lecturer.first_name
    lecturer_last_name = lecturer.lecturer.last_name
    print("lecturer name: {} {}".format(
        lecturer_first_name, lecturer_last_name))
    return "{} {}".format(lecturer_first_name, lecturer_last_name)

def get_courses_assigned_to_a_lecturer(lecturer):
    lecturer_course = Lecture_Course.objects.filter(lecturer=lecturer)
    print('lecturer course: {}'.format(lecturer_course))
    courses = get_list_of_courses_for_a_lecturer(lecturer_course)
    print("number of courses assigned to {}: {}".format(
        get_lecturer_name(lecturer), len(courses)))
    print(courses)
    return courses

def get_list_of_courses_for_a_lecturer(list_of_lecturer_course_instance):
    courses = []

    for lecturer_course_instance in list_of_lecturer_course_instance:
        courses.append(
            {
                "course_code": lecturer_course_instance.course.course_code,
                "course_description": lecturer_course_instance.course.course_name
            }
        )
    return courses

def get_courses_as_list(token):
    user = get_user_from_token(token)
    lecturer = get_lecturer_from_user(user)
    courses = get_courses_assigned_to_a_lecturer(lecturer)

    courses_list = []
    for course in courses:
        courses_list.append(course["course_code"])

    return courses_list

#
#
# lecturer details api
#
#

@api_view(["GET"])
@permission_classes([IsAuthenticated])
def lecturer_details(request):
    usernames = [user.username for user in User.objects.all()]
    print("token from request header: {}".format(
        request.META.get('HTTP_AUTHORIZATION')))
    authorization_token = request.META.get('HTTP_AUTHORIZATION')
    token = authorization_token[6:]

    try:
        user = get_user_from_token(token)
        lecturer = get_lecturer_from_user(user)
        lecturer_name = get_lecturer_name(lecturer)
        courses = get_courses_assigned_to_a_lecturer(lecturer)

        return Response({
            "user_name": lecturer.lecturer.username,
            "lecturer_name": lecturer_name,
            "courses_teaching": courses,
            "course_count": len(courses),
            "position": lecturer.role.role_name,
        })
    except ObjectDoesNotExist as error:
        print("no lecturer with such credentials")
        print("error message: {}".format(error))

    print("usernames: {}".format(usernames))

    return Response("Nothing to show")

#
#
# end lecturer api
#
#

#
#
# assessment results api
#
#

@api_view(["GET", "POST"])
@permission_classes([IsAuthenticated])
def assessment_results(request):
    authorization_token = request.META.get('HTTP_AUTHORIZATION')
    token = authorization_token[6:]

    if request.method == 'GET':
        try:
            courses_list = get_courses_as_list(token)
            assessments = Assessment.objects.filter(course__in=courses_list)
            assessment_results = Assessment_Results.objects.filter(
                assessment__in=assessments)
            serializer = AssessmentResultsSerializer(
                assessment_results, many=True)
            return Response(serializer.data)

        except ObjectDoesNotExist as error:
            print("error message: {}".format(error))

    elif request.method == "POST":
        serializer = AssessmentResultsSerializer(data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@api_view(["PUT"])
@permission_classes([IsAuthenticated])
def assessment_results_update(request, pk):
    try:
        assessment_result = Assessment_Results.objects.get(pk=pk)
    except Assessment_Results.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    serializer = AssessmentResultsSerializer(
        assessment_result, data=request.data)

    if serializer.is_valid():
        serializer.save()
        return Response(serializer.data)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

#
#
# end assessment results api
#
#

#
#
# assessment details api
#
#

@api_view(["GET", "POST"])
@permission_classes([IsAuthenticated])
def assessment_details(request):
    authorization_token = request.META.get('HTTP_AUTHORIZATION')
    token = authorization_token[6:]

    if request.method == 'GET':
        try:
            user = get_user_from_token(token)
            lecturer = get_lecturer_from_user(user)
            courses = get_courses_assigned_to_a_lecturer(lecturer)

            courses_list = []
            for course in courses:
                courses_list.append(course["course_code"])

            assessments = Assessment.objects.filter(course__in=courses_list)
            serializer = AssessmentSerializer(assessments, many=True)
            return Response(serializer.data)

        except ObjectDoesNotExist as error:
            print("error message: {}".format(error))

    elif request.method == "POST":
        serializer = AssessmentSerializer(data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@api_view(["PUT", "DELETE"])
@permission_classes([IsAuthenticated])
def assessment_details_update_delete(request, pk):
    try:
        assessment = Assessment.objects.get(pk=pk)
    except Assessment.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == "DELETE":
        assessment.delete()
        return Response({"message": "deleted successfully", "verified_by": "django"}, status=status.HTTP_204_NO_CONTENT)

    elif request.method == 'PUT':
        serializer = AssessmentSerializer(assessment, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

#
#
# end assessment details api
#
#

def export_users_xls(request):
    response = HttpResponse(content_type='application/ms-excel')
    response['Content-Disposition'] = 'attachment; filename="users.xls"'

    wb = xlwt.Workbook(encoding='utf-8')
    ws = wb.add_sheet('Users Data')  # this will make a sheet named Users Data

    # writing the first row
    alignment = xlwt.Alignment()
    alignment.horz = xlwt.Alignment.HORZ_CENTER
    look = xlwt.Font()
    look.bold = True
    horz_style = xlwt.XFStyle()
    horz_style.alignment = alignment
    horz_style.font = look

    top_row = 1
    bottom_row = 1
    left_column = 0
    right_column = 3
    ws.write_merge(top_row, bottom_row, left_column, right_column,
                   'UNIVERSITY OF DAR ES SALAAM', horz_style)
    # end of first row

    # writing the second row
    alignment = xlwt.Alignment()
    alignment.horz = xlwt.Alignment.HORZ_CENTER
    look = xlwt.Font()
    look.bold = False
    horz_style = xlwt.XFStyle()
    horz_style.alignment = alignment
    horz_style.font = look

    top_row = 3
    bottom_row = 3
    left_column = 0
    right_column = 3
    ws.write_merge(top_row, bottom_row, left_column, right_column, 'SUMMARY OF RESULTS (PAPERS/COURSES/PRACTICAL/ORAL)',
                   horz_style)
    # end of second row

    # writing programme
    row_num = 5
    col_num = 0
    ws.write(row_num, col_num, "CEIT", xlwt.XFStyle())
    # end of writing programme

    # writing year of study
    row_num = 5
    col_num = 2
    ws.write(row_num, col_num, "YEAR OF STUDY", xlwt.XFStyle())
    # end of writing year of study
    #
    # writing year of study value
    row_num = 5
    col_num = 3
    style = xlwt.XFStyle()
    bold_style = xlwt.Font()
    bold_style.bold = True
    style.font = bold_style
    style.alignment = alignment
    ws.write(row_num, col_num, "III", style)
    # end of writing year of study value

    # Sheet header, first row
    row_num = 10

    font_style = xlwt.XFStyle()
    font_style.font.bold = True

    columns = ['Username', 'First Name', 'Last Name', 'Email Address', ]

    for col_num in range(len(columns)):
        cwidth = ws.col(col_num).width
        if (len(columns[col_num]) * 367) > cwidth:
            ws.col(col_num).width = (len(columns[col_num]) * 367)
        # at 0 row 0 column
        ws.write(row_num, col_num, columns[col_num], font_style)

    # Sheet body, remaining rows
    font_style = xlwt.XFStyle()

    rows = User.objects.all().values_list(
        'username', 'first_name', 'last_name', 'email')
    for row in rows:
        row_num += 1
        for col_num in range(len(row)):
            ws.write(row_num, col_num, row[col_num], font_style)

    wb.save(response)

    context = {}

    return render(request, 'backendapi/excel_home.html', context)


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

"""
This module provides *general* utility functions used by the **grains** package. The specific
helper functions reside in the proper module. For example, a function that works on a general
list goes here, but a computational geometry algorithm goes to the **geometry** module. The
functions in the **utils** module can be interesting for other projects too, partly because of
their general scope, and partly because of the few dependencies.

Functions
---------
.. autosummary::
    :nosignatures:
    :toctree: functions/

    duplicates
    toggle
    index_list
    flatten_list
    argsorted
    map_inplace
    non_unique
    parse_kwargs
    compress
    decompress
    decompress_inmemory
    neighborhood

"""

import os
import zipfile
from functools import reduce

import numpy as np

def duplicates(sequence):
    """Set of duplicate elements in a sequence.

    Parameters
    ----------
    sequence : sequence types (list, tuple, string, etc.)
        Sequence possibly containing repeating elements.

    Returns
    -------
    set
        Set of unique values.

    Notes
    -----
    Copied from https://stackoverflow.com/a/9836685/4892892

    Examples
    --------
    Note that the order of the elements in the resulting set does not matter.

    >>> a = [1, 2, 3, 2, 1, 5, 6, 5, 5, 5]  # list
    >>> duplicates(a)
    {1, 2, 5}
    >>> a = (1, 1, 0, -1, -1, 0)  # tuple
    >>> duplicates(a)
    {0, 1, -1}
    >>> a = 'abbcdkc'  # string
    >>> duplicates(a)
    {'c', 'b'}
    """
    seen = set()
    seen_add = seen.add
    # adds all elements it doesn't know yet to seen and all other to seen_twice
    seen_twice = set(x for x in sequence if x in seen or seen_add(x))
    # turn the set into a list (as requested)
    return seen_twice

def toggle(lst):
    """Return True for False values and False for True values in a list.

    Parameters
    ----------
    lst : list
        An arbitrary list, possibly containing other lists.

    Returns
    -------
    list
        Element-wise logical not operator applied on the input list.

    Notes
    -----
    Solution taken from https://stackoverflow.com/a/51122372/4892892.

    Examples
    --------
    >>> toggle([True, False])
    [False, True]
    >>> toggle(['h', 0, 2.3, -2, 5, []])
    [False, True, False, False, False, True]

    """
    return list(map(lambda item: not item, lst))

def index_list(lst, indices):
    """Index a list by another list.

    Parameters
    ----------
    lst : list
        List to be indexed.
    indices : list
        Indices of the original list that will form the new list.

    Returns
    -------
    list
        Members of `lst`, selected by `indices`.

    Examples
    --------
    >>> index_list(['c', ['nested', 'list'], 13], [1, 2])
    [['nested', 'list'], 13]

    """
    return [lst[idx] for idx in indices]

def flatten_list(nested_list):
    """Merge a list of lists to a single list.

    Parameters
    ----------
    nested_list : list
        List containing other lists.

    Returns
    -------
    list
        Flattened list.

    Notes
    -----
    - Only a single level (i.e. list of lists) is handled, see the second example.
    - Several methods, such as list comprehension, monoid and loops, are proposed in
      https://stackoverflow.com/questions/952914/how-to-make-a-flat-list-out-of-list-of-lists.
      Here, the list comprehension approach is used.

    Examples
    --------
    >>> nested_list = [['some'], ['items']]
    >>> flatten_list(nested_list)
    ['some', 'items']
    >>> multiply_nested_list = [[['item'], 'within', 'item']]
    >>> flatten_list(multiply_nested_list)
    [['item'], 'within', 'item']

    """
    return [item for sublist in nested_list for item in sublist]

def argsorted(sequence, reverse=False):
    """Return the indices that would sort a list or a tuple.

    Implementation is taken from https://stackoverflow.com/a/6979121/4892892.

    Parameters
    ----------
    sequence : list, tuple
        Input sequence in which the sorted indices to be found.
    reverse : bool
        If set to True, then the elements are sorted as if each comparison was reversed.

    Returns
    -------
    list
        List of indices that would sort the input list/tuple.

    See Also
    --------
    :func:`sorted`
    :func:`numpy.argsort`

    Examples
    --------
    >>> argsorted([2, 1.1, 1.1])
    [1, 2, 0]
    >>> argsorted([2, 1.1, 1.1], reverse=True)
    [0, 1, 2]
    >>> argsorted(())
    []

    """
    return sorted(range(len(sequence)), key=lambda k: sequence[k], reverse=reverse)

def map_inplace(function, __iterable):
    """Apply a function to each member of an iterable in-place.

    Parameters
    ----------
    function : function object
        Function to be applied to the entries of the iterable.
    __iterable : iterable
        Iterable.

    Notes
    -----
    Comprehensions or functional tools work on iterators, thereby not modifying the original
    container (https://stackoverflow.com/a/4148523/4892892). For in-place modification, the
    conventional for loop approach is used (https://stackoverflow.com/a/4148525/4892892).

    Examples
    --------
    >>> lst = ['2', 2]; func = lambda x: x*2
    >>> map_inplace(func, lst); lst
    ['22', 4]
    >>> lifespan = {'cat': 15, 'dog': 12}; die_early = lambda x: x/2
    >>> map_inplace(die_early, lifespan); lifespan
    {'cat': 7.5, 'dog': 6.0}

    """
    if type(__iterable) == dict:
        for key, value in __iterable.items():
            __iterable[key] = function(value)
    else:
        for i in range(len(__iterable)):
            __iterable[i] = function(__iterable[i])

def non_unique(array, axis=None):
    """Finds indices of non-unique elements in a 1D or 2D ndarray.

    Parameters
    ----------
    array : ndarray
        Array in which the non-unique elements are searched.
    axis : {None, 0, 1}, optional
        The axis to operate on. If None, `array` will be flattened. If an integer, the subarrays
        indexed by the given axis will be flattened and treated as the elements of a 1-D array with
        the dimension of the given axis. Object arrays or structured arrays that contain objects are
        not supported if the `axis` kwarg is used. The default is None.

    Returns
    -------
    nonunique_values : list
        Unique (individual, row or column) entries.
    nonunique_indices : list
        Each element of the list corresponds to non-unique elements, whose indices are given in
        a 1D numpy array.

    Examples
    --------
    In a 1D array, the repeated values and their indices are found by

    >>> val, idx = non_unique(np.array([1, -1, 0, -1, 2, 5, 0, -1]))
    >>> val
    [-1, 0]
    >>> idx
    [array([1, 3, 7]), array([2, 6])]

    In the matrix below, we can see that rows 0 and 2 are identical, as well as rows 1 and 4.

    >>> val, idx = non_unique(np.array([[1, 3], [2, 4], [1, 3], [-1, 0], [2, 4]]), axis=0)
    >>> val
    [array([1, 3]), array([2, 4])]
    >>> idx
    [array([0, 2]), array([1, 4])]

    By transposing the matrix above, the same holds for the columns.

    >>> val, idx = non_unique(np.array([[1, 2, 1, -1, 2], [3, 4, 3, 0, 4]]), axis=1)
    >>> val
    [array([1, 3]), array([2, 4])]
    >>> idx
    [array([0, 2]), array([1, 4])]

    If the dimensions along which to find the duplicates are not given, the input is flattened
    and the indexing happens in C-order (row-wise).

    >>> val, idx = non_unique(np.array([[1, 2, 1, -1, 2], [3, 4, 3, 0, 4]]))
    >>> val
    [1, 2, 3, 4]
    >>> idx
    [array([0, 2]), array([1, 4]), array([5, 7]), array([6, 9])]

    """
    if axis not in {None, 0, 1}:
        raise Exception('Parameter `axis` must be one of the following: None, 0, 1.')
    # Indices of the repeated elements
    _, idx, counts = np.unique(array, axis=axis, return_counts=True, return_inverse=True)
    distinct_indices = np.unique(idx)
    repeated_indices = distinct_indices[counts > 1]
    # Help in consistent indexing
    if axis is None:
        array = array.flatten()
    elif axis == 0:
        pass
    elif axis == 1:
        array = array.T
    # Find the repeated values and their positions at the same time
    nonunique_indices = []
    nonunique_values = []
    for i in repeated_indices:
        nonunique_indices.append(np.nonzero(idx == i)[0])
        nonunique_values.append(array[idx == i][0])
    return nonunique_values, nonunique_indices

def parse_kwargs(kwargs, defaults):
    """Compares keyword arguments with defaults.

    Allows processing keyword arguments supplied to a function by the user by comparing them with
    an admissible set of options defined by the developer. There are three cases:

        1. The keyword given by the user is present in the set the developer provides. Then the
        value belonging to the keyword overrides the default value.

        2. The keyword given by the user is `not` present in the set the developer provides. In
        this case, the unrecognized keyword, along with its value, is saved separately.

        3. The keyword existing in the set the developer provides is not given by the user. Then
        the default value is used.

    Parameters
    ----------
    kwargs : dict
        Keyword arguments (parameter-value pairs) passed to a function.
    defaults : dict
        Default parameter-value pairs.

    Returns
    -------
    parsed : dict
        Dictionary with the same keys as :code:`defaults`, the parsed parameter-value pairs.
    unknown : dict
        Dictionary containing the parameter-value pairs not present in :code:`defaults`.

    Notes
    -----
    The default values, given in the input dictionary :code:`defaults`, are never overwritten.

    Examples
    --------
    >>> default_options = {'opt1': 1, 'opt2': 'string', 'opt3': [-1, 0]}
    >>> user_options = {'opt3': [2, 3, -1], 'opt2': 'string',  'opt4': -2.14}
    >>> parsed_options, unknown_options = parse_kwargs(user_options, default_options)
    >>> parsed_options
    {'opt1': 1, 'opt2': 'string', 'opt3': [2, 3, -1]}
    >>> unknown_options
    {'opt4': -2.14}

    """
    parsed = defaults.copy()
    unknown = {}
    for param, value in kwargs.items():
        if param in defaults:
            parsed[param] = value
        else:
            unknown[param] = value
    return parsed, unknown

def compress(filename, level=9):
    """Creates a zip archive from a single file.

    Parameters
    ----------
    filename : str
        Name of the file to be compressed.
    level : int, optional
        Level of compression. Integers 0 through 9 are accepted. The default is 9.

    Returns
    -------
    None.

    See Also
    --------
    :class:`zipfile.ZipFile`

    """
    name = os.path.splitext(filename)[0]
    output_file = name + '.zip'
    zip_options = dict(compression=zipfile.ZIP_DEFLATED, compresslevel=level)
    with zipfile.ZipFile(output_file, mode='w', **zip_options) as compressed:
        compressed.write(filename)

def decompress(filename, path=None):
    """Decompresses the contents of a zip archive into the current directory.

    Parameters
    ----------
    filename : str
        Name of the zip archive.

    path : str, optional
        Directory to extract to. The default is the directory the function is called from.

    See Also
    --------
    :class:`zipfile.ZipFile`

    """
    with zipfile.ZipFile(filename, mode='r') as compressed:
        compressed.extractall(path=path)

def decompress_inmemory(filename):
    """Decompresses the contents of a zip archive into a dictionary.

    Parameters
    ----------
    filename : str
        Name of the zip archive.

    Returns
    -------
    data : dict
        The keys of the dictionary are the compressed file names (without extension),
        the corresponding values are their contents.

    See Also
    --------
    :class:`zipfile.ZipFile`

    """
    data = {}
    with zipfile.ZipFile(filename, mode='r') as compressed:
        for file in compressed.namelist():
            with compressed.open(file, mode='r') as thisfile:
                name = os.path.splitext(thisfile.name)[0]
                data[name] = compressed.read(thisfile.name).decode()
    return data

def neighborhood(center, radius, norm, method='ball', bounds=None):
    r"""Neighboring points to a grid point.

    Given a point in a subspace of :math:`\mathbb{Z}^n`, the neighboring points are determined
    based on the specified rules.

    .. todo::
        Currently, the neighbors are deterministically but not systematically ordered.
        Apart from testing purposes, this does not seem to be a big issue.
        Still, a logical ordering is desirable. E.g. ordering in increasing coordinate values,
        first in the first dimension and lastly in the last dimension.

    Parameters
    ----------
    center : tuple of int
        Point :math:`x_0` around which the neighborhood is searched. It must be an n-tuple of
        integers, where :math:`n` is the spatial dimension.
    radius : int, positive
        Radius of the ball or sphere in which the neighbors are searched.
        If the radius is 1, the immediate neighbors are returned.
    norm : {1, inf}
        Type of the vector norm :math:`\| x - x_0 \|`, where :math:`x` is a point whose
        distance is searched from the center :math:`x_0`.

        =========   ============   =========================================================
        Type        Name           Definition
        =========   ============   =========================================================
        1           1-norm         :math:`\| x \|_1 = \sum_{i=1}^n |x_i|`
        numpy.inf   maximum norm   :math:`\| x \|_{\infty} = \max\{ |x_1|, \ldots, |x_n| \}`
        =========   ============   =========================================================

        where inf means numpy's np.inf object.
    method : {'ball', 'sphere'}, optional
        Specifies the criterion of how to decide whether a point :math:`x` is in the neighborhood
        of :math:`x_0`. The default is 'ball'.

        For 'ball':

        .. math::
            \| x - x_0 \| \leq r

        For 'sphere':

        .. math::
            \| x - x_0 \| = r

        where :math:`r` is the radius passed as the :code:`radius` parameter and the type of the
        norm is taken based on the :code:`norm` input parameter.
    bounds : list of tuple, optional
        Restricts the neighbors within a box. The dimensions of the n-dimensional box are given
        as a list of 2-tuples: [(x_1_min, x_1_max), ... , (x_n_min, x_n_max)]. The default value
        is an unbounded box in all dimensions. Use np.inf to indicate unbounded values.

    Returns
    -------
    neighbors : tuple of ndarray
        Tuple of length n, each entry being a 1D numpy array: the integer indices of the points
        that are in the neighborhood of :code:`center`.

    Notes
    -----
    1. The `von Neumann neighborhood <https://mathworld.wolfram.com/vonNeumannNeighborhood.html>`_
    with range :math:`r` is a special case when :code:`radius=r`, :code:`norm=1` and
    :code:`method='ball'`.

    2. The `Moore neighborhood <https://mathworld.wolfram.com/MooreNeighborhood.html>`_
    with range :math:`r` is a special case when :code:`radius=r`, :code:`norm=np.inf` and
    :code:`method='ball'`.

    Examples
    --------
    Find the Moore neighborhood with range 2 the point (1) on the half-line [0, inf).

    >>> neighborhood((1,), 2, np.inf, bounds=[(0, np.inf)])
    (array([0, 1, 2, 3]),)

    Find the von Neumann neighborhood with range 2 around the point (2, 2), restricted on
    the domain [0, 4] x [0, 3].

    >>> neighborhood((2, 2), 2, 1, bounds=[(0, 4), (0, 3)])
    (array([2, 1, 2, 3, 0, 1, 2, 3, 4, 1, 2, 3]), array([0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3]))

    Find the Moore neighborhood with range 1 around the point (0, -4) such that the neighbors
    lie on the half-plane [0, 2] x (-inf, -4].

    >>> neighborhood((0, -4), 1, np.inf, bounds=[(0, 2), (-np.inf, -4)])
    (array([0, 1, 0, 1]), array([-5, -5, -4, -4]))

    Find the sphere of radius 2, measured in the 1-norm, around the point (-1, 0, 3), within the
    half-space {(x,y,z) in Z^3 | y>=0}.

    >>> neighborhood((-1, 0, 3), 2, 1, 'sphere', [(-np.inf, np.inf), (0, np.inf), (-np.inf, np.inf)])  # doctest: +NORMALIZE_WHITESPACE
    (array([-3, -2, -2, -1, -1,  0,  0,  1, -2, -1, -1,  0, -1]),
     array([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2]),
     array([3, 2, 4, 1, 5, 2, 4, 3, 3, 2, 4, 3, 3]))

    """
    dim = len(center)
    if not bounds:
        bounds = [(-np.Inf, np.Inf) for i in range(dim)]

    # Every neighborhood is within this bounding box
    bounding_box = [np.linspace(center[i] - radius, center[i] + radius, 2 * radius + 1, dtype=int)
                    for i in range(dim)]
    # Coordinates of the points in that bounding box
    X = np.meshgrid(*bounding_box)
    candidates = [X[i].flatten() for i in range(dim)]

    # Distance of the candidate points from the center
    candidates_matrix = np.column_stack(candidates)
    center_matrix = np.reshape(center, (1, dim))
    n_candidates = (2 * radius + 1) ** dim
    center_matrix = np.repeat(center_matrix, n_candidates, axis=0)
    distance = lambda x: np.linalg.norm(x, norm, axis=1)
    distances = distance(candidates_matrix - center_matrix)

    # Select those points that satisfy the required distance measure
    within_distance = distances <= radius if method == 'ball' else distances == radius

    # Consider only neighbors that fall within the specified bounds
    within_bounds = []
    for i in range(dim):
        lower_bound = bounds[i][0]
        upper_bound = bounds[i][1]
        within_bounds.append((candidates_matrix[:, i] >= lower_bound) &
                             (candidates_matrix[:, i] <= upper_bound))
    within_bounds = reduce(np.logical_and, within_bounds)

    # Neighbors are given component-wise
    neighbors = tuple(candidates[i][within_distance & within_bounds] for i in range(dim))
    return neighbors

if __name__ == "__main__":
    import doctest
    doctest.testmod(verbose=True)


"""
Simple script to read & write Source Documents in either YAML or raw text
formats
"""

from argparse import ArgumentParser, Namespace

from pii_data.helper.io import base_extension
from pii_data.types.localdoc import LocalSrcDocumentFile

from ..doc.text import TextSrcDocument, CHUNK_MODES

def from_plain(inputfile: str, args: Namespace) -> TextSrcDocument:
    """
    Read a plain text file
    """
    opt = {"mode": args.mode}
    if args.input_indent:
        opt["indent"] = args.input_indent
    if args.chunk_options:
        opt.update(v.split("=", 1) for v in args.chunk_options)
    return TextSrcDocument(inputfile, chunk_options=opt)

# --------------------------------------------------------------------------

def parse_args():
    args = ArgumentParser(description='Convert from YAML PII Source Doc to plain raw text or viceversa')
    args.add_argument('inputdoc', help='input file (text or YAML)')
    args.add_argument('outputdoc',
                      help='output file (format to be deduced from file extension)')
    args.add_argument('--mode', choices=CHUNK_MODES, default="line",
                      help="text chunking mode (default: %(default)s)")
    args.add_argument('--chunk-options', metavar="NAME=VAL", nargs="+",
                      help="text chunking options")
    args.add_argument('--input-indent', type=int, default=0, metavar="NUMCHARS",
                      help="indent value to detect tree hierarchy in text input")
    args.add_argument('--output-indent', type=int, default=None,
                      metavar="NUMCHARS",
                      help="output indent value (for text or json output)")
    return args.parse_args()

def main(args: Namespace = None):

    if not args:
        args = parse_args()

    # Read document
    ext1 = base_extension(args.inputdoc)
    if ext1 in ('.yml', '.yaml'):
        doc = LocalSrcDocumentFile(args.inputdoc)
    else:
        doc = from_plain(args.inputdoc, args)

    # Write it
    doc.dump(args.outputdoc, indent=args.output_indent)

if __name__ == '__main__':
    main()