import tensorflow as tf

import numpy as np
from functools import reduce

VGG_MEAN = [103.939, 116.779, 123.68]


class vggVlad:
    """
    A trainable version VGG19.
    """

    def __init__(self, vgg19_npy_path=None, dropout=0.5):
        if vgg19_npy_path is not None:
            self.data_dict = np.load(vgg19_npy_path, encoding='latin1').item()
        else:
            self.data_dict = None

        self.var_dict = {}
        self.dropout = dropout

    def build(self, rgb, train_mode=None):
        """
        load variable from npy to build the VGG

        :param rgb: rgb image [batch, height, width, 3] values scaled [0, 1]
        :param train_mode: a bool tensor, usually a placeholder: if True, dropout will be turned on
        """

        rgb_scaled = rgb * 255.0

        # Convert RGB to BGR
        red, green, blue = tf.split(axis=3, num_or_size_splits=3, value=rgb_scaled)
        assert red.get_shape().as_list()[1:] == [224, 224, 1]
        assert green.get_shape().as_list()[1:] == [224, 224, 1]
        assert blue.get_shape().as_list()[1:] == [224, 224, 1]
        bgr = tf.concat(axis=3, values=[
            blue - VGG_MEAN[0],
            green - VGG_MEAN[1],
            red - VGG_MEAN[2],
        ])

        assert bgr.get_shape().as_list()[1:] == [224, 224, 3]

        self.conv1_1 = self.conv_layer(bgr, 3, 64, "conv1_1")
        self.conv1_2 = self.conv_layer(self.conv1_1, 64, 64, "conv1_2")
        self.pool1 = self.max_pool(self.conv1_2, 'pool1')

        self.conv2_1 = self.conv_layer(self.pool1, 64, 128, "conv2_1")
        self.conv2_2 = self.conv_layer(self.conv2_1, 128, 128, "conv2_2")
        self.pool2 = self.max_pool(self.conv2_2, 'pool2')

        self.conv3_1 = self.conv_layer(self.pool2, 128, 256, "conv3_1")
        self.conv3_2 = self.conv_layer(self.conv3_1, 256, 256, "conv3_2")
        self.conv3_3 = self.conv_layer(self.conv3_2, 256, 256, "conv3_3")
        self.conv3_4 = self.conv_layer(self.conv3_3, 256, 256, "conv3_4")
        self.pool3 = self.max_pool(self.conv3_4, 'pool3')

        self.conv4_1 = self.conv_layer(self.pool3, 256, 512, "conv4_1")
        self.conv4_2 = self.conv_layer(self.conv4_1, 512, 512, "conv4_2")
        self.conv4_3 = self.conv_layer(self.conv4_2, 512, 512, "conv4_3")
        self.conv4_4 = self.conv_layer(self.conv4_3, 512, 512, "conv4_4")
        self.pool4 = self.max_pool(self.conv4_4, 'pool4')

        self.conv5_1 = self.conv_layer(self.pool4, 512, 512, "conv5_1")
        self.conv5_2 = self.conv_layer(self.conv5_1, 512, 512, "conv5_2")
        self.conv5_3 = self.conv_layer(self.conv5_2, 512, 512, "conv5_3")
        self.conv5_4 = self.conv_layer_no_relu(self.conv5_3, 512, 512, "conv5_4")

        self.vlad = self.vlad_layer(self.conv5_4, 64, "vlad", True)
        self.reduceVec = tf.reduce_sum(self.conv5_4, [1,2])

        self.data_dict = None

    def avg_pool(self, bottom, name):
        return tf.nn.avg_pool(bottom, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME', name=name)

    def max_pool(self, bottom, name):
        return tf.nn.max_pool(bottom, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME', name=name)

    def conv_layer(self, bottom, in_channels, out_channels, name, trainable = False):
        with tf.variable_scope(name):
            filt, conv_biases = self.get_conv_var(3, in_channels, out_channels, name, trainable)

            conv = tf.nn.conv2d(bottom, filt, [1, 1, 1, 1], padding='SAME')
            bias = tf.nn.bias_add(conv, conv_biases)
            relu = tf.nn.relu(bias)

            return relu

    def conv_layer_no_relu(self, bottom, in_channels, out_channels, name, trainable = False):
        with tf.variable_scope(name):
            filt, conv_biases = self.get_conv_var(3, in_channels, out_channels, name, trainable)

            conv = tf.nn.conv2d(bottom, filt, [1, 1, 1, 1], padding='SAME')
            bias = tf.nn.bias_add(conv, conv_biases)
            norm = tf.nn.l2_normalize(bias, 3)
            return norm

    def vlad_layer(self, bottom, k_center, name, trainable = False):

        num_batches = bottom.get_shape()[0].value
        num_features = bottom.get_shape()[1].value * bottom.get_shape()[2].value
        dim_feature = bottom.get_shape()[3].value

        bottom = tf.reshape(bottom, [num_batches, num_features, dim_feature]) # B * N * D

        with tf.variable_scope(name):
            self.vlad_w, self.vlad_b, self.vlad_c = self.get_vlad_var([k_center, dim_feature], [k_center, 1], [k_center, dim_feature], name, trainable)

            w = self.vlad_w
            bias = self.vlad_b
            c = self.vlad_c

            self.wt = tf.transpose(w, perm = [1,0]) # D * K
            # (N * D) * (D * K)
            t = tf.matmul(bottom, tf.tile(tf.expand_dims(self.wt,0), multiples=[num_batches, 1, 1])) # BNK

            bias = tf.reshape(bias, [k_center])
            bias = tf.tile(tf.expand_dims(bias, 0), [num_features, 1])
            bias = tf.tile(tf.expand_dims(bias, 0), [num_batches, 1, 1])

            t = tf.add(t, bias)

            self.numerator = tf.exp(t) # B N K
            self.denominator = tf.reduce_sum(self.numerator, 2) # B N

            for k in range(k_center):
                self.alpha = tf.divide(self.numerator[:,:,k], self.denominator) # B N
                self.alpha = tf.expand_dims(self.alpha, -1)

                if k == 0:
                    outputs = tf.reduce_sum(tf.multiply(self.alpha, (bottom - tf.slice(c, [k, 0], [1, dim_feature]))), axis=1) 
                    outputs = tf.expand_dims(outputs,1) # B1D 
                else:
                    outputs = tf.concat([outputs, tf.expand_dims(tf.reduce_sum(tf.multiply(self.alpha, (bottom - tf.slice(c,[k, 0], [1, dim_feature]))), axis=1),1)], 1)

            outputs = tf.nn.l2_normalize(outputs,dim=2) #intra-normalization B * K * D
            outputs = tf.reshape(outputs,[num_batches,-1])
            outputs = tf.nn.l2_normalize(outputs,dim=1) #l2 normalization
            return outputs

    def fc_layer(self, bottom, in_size, out_size, name):
        with tf.variable_scope(name):
            weights, biases = self.get_fc_var(in_size, out_size, name)

            x = tf.reshape(bottom, [-1, in_size])
            fc = tf.nn.bias_add(tf.matmul(x, weights), biases)

            return fc

    def get_vlad_var(self, wshape, bshape, cshape, name, trainable = False):
        initial_value = tf.truncated_normal(wshape, 1.0, .001)
        weights = self.get_var(initial_value, name, 0, name + "_weights", trainable)
        self.vlad_name_w = name + "_weights"

        initial_value = tf.truncated_normal(bshape, .0, .001)
        biases = self.get_var(initial_value, name, 1, name + "_biases", trainable)
        self.vlad_name_b = name + "_biases"

        initial_value = tf.truncated_normal(cshape, .5, .01)
        centers = self.get_var(initial_value, name, 2, name + "_centers", trainable)
        self.vlad_name_c = name + "_centers"

        return weights, biases, centers

    def get_conv_var(self, filter_size, in_channels, out_channels, name, trainable = False):
        initial_value = tf.truncated_normal([filter_size, filter_size, in_channels, out_channels], 0.0, 0.001)
        filters = self.get_var(initial_value, name, 0, name + "_filters", trainable)

        initial_value = tf.truncated_normal([out_channels], .0, .001)
        biases = self.get_var(initial_value, name, 1, name + "_biases", trainable)

        return filters, biases

    def get_fc_var(self, in_size, out_size, name):
        initial_value = tf.truncated_normal([in_size, out_size], 0.0, 0.001)
        weights = self.get_var(initial_value, name, 0, name + "_weights")

        initial_value = tf.truncated_normal([out_size], .0, .001)
        biases = self.get_var(initial_value, name, 1, name + "_biases")

        return weights, biases

    def get_var(self, initial_value, name, idx, var_name, trainable = False):
        if self.data_dict is not None and name in self.data_dict:
            value = self.data_dict[name][idx]
        else:
            value = initial_value

        if trainable:
            var = tf.Variable(value, name=var_name)
        else:
            var = tf.constant(value, dtype=tf.float32, name=var_name)

        self.var_dict[(name, idx)] = var

        # print var_name, var.get_shape().as_list()
        assert var.get_shape() == initial_value.get_shape()

        return var

    def save_npy(self, sess, npy_path="./vgg19-save.npy"):
        assert isinstance(sess, tf.Session)

        data_dict = {}

        for (name, idx), var in list(self.var_dict.items()):
            var_out = sess.run(var)
            if name not in data_dict:
                data_dict[name] = {}
            data_dict[name][idx] = var_out

        np.save(npy_path, data_dict)
        print(("file saved", npy_path))
        return npy_path

    def get_var_count(self):
        count = 0
        for v in list(self.var_dict.values()):
            count += reduce(lambda x, y: x * y, v.get_shape().as_list())
        return count



