import time
import numpy as np
import tensorflow as tf
from tensorflow.contrib import slim

tf.app.flags.DEFINE_integer('input_size', 512, '')
tf.app.flags.DEFINE_integer('batch_size_per_gpu', 10, '')
tf.app.flags.DEFINE_integer('num_readers', 4, '')
tf.app.flags.DEFINE_float('learning_rate', 0.0001, '')
tf.app.flags.DEFINE_integer('max_steps', 101000, '')
tf.app.flags.DEFINE_float('moving_average_decay', 0.997, '')
tf.app.flags.DEFINE_string('gpu_list', '5', '')

tf.app.flags.DEFINE_string('checkpoint_path', './GAnet_Vgg16_B10', 'Save the model')


tf.app.flags.DEFINE_boolean('restore', False, 'whether to resotre from checkpoint')
tf.app.flags.DEFINE_integer('save_checkpoint_steps', 2000, '')
tf.app.flags.DEFINE_integer('save_summary_steps', 2000, '')

#Fixme: https://github.com/tensorflow/models/tree/master/research/slim
#Fixme: Please download the pretrianed model Vgg16 and Resnet_50, and move them to the folder model_vgg_16 and model_resnet_50.
# tf.app.flags.DEFINE_string('pretrained_model_path', 'model_resnet_50/resnet_v1_101.ckpt', '')
# tf.app.flags.DEFINE_string('pretrained_model_path', 'model_resnet_50/resnet_v1_50.ckpt', '')
tf.app.flags.DEFINE_string('pretrained_model_path', 'model_vgg_16/vgg_16.ckpt', '')
# tf.app.flags.DEFINE_string('pretrained_model_path', 'resnet_v1_50_2016_08_28/resnet_v1_50.ckpt', '')

import GAnet_model_UAV_Vgg16 as model
import GAnet_data as data

FLAGS = tf.app.flags.FLAGS

gpus = list(range(len(FLAGS.gpu_list.split(','))))


#Fixme: classification (add labels)
def tower_loss(images, labels, score_maps, ps_maps, ps_score_maps, geo_maps, training_masks, reuse_variables=None):
    # Build inference graph
    with tf.variable_scope(tf.get_variable_scope(), reuse=reuse_variables):
        f_score, f_geometry, F_score_KR, logits = model.model(images, is_training=True)

    model_loss = model.loss_KR(score_maps, f_score,
                            geo_maps, f_geometry,
                            ps_score_maps, F_score_KR,
                            training_masks, ps_maps)

    #Fixme: For weak supervision (classification loss)
    cls_loss = 0.0
    cls_loss += tf.reduce_mean(
        tf.nn.sparse_softmax_cross_entropy_with_logits(
            logits=logits, labels=labels))  # groundtruth_lists

    model_loss += cls_loss * 0.001


    total_loss = tf.add_n([model_loss] + tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES))

    # add summary
    # if reuse_variables is None:
        # tf.summary.image('input', images)
        # tf.summary.image('score_map', score_maps)
        # tf.summary.image('score_map_pred', f_score * 255)
        # tf.summary.image('geo_map_0', geo_maps[:, :, :, 0:1])
        # tf.summary.image('geo_map_0_pred', f_geometry[:, :, :, 0:1])
        # tf.summary.image('training_masks', training_masks)
        # tf.summary.scalar('model_loss', model_loss)
        # tf.summary.scalar('total_loss', total_loss)

    return total_loss, model_loss


def average_gradients(tower_grads):
    average_grads = []
    for grad_and_vars in zip(*tower_grads):
        grads = []
        for g, _ in grad_and_vars:
            expanded_g = tf.expand_dims(g, 0)
            grads.append(expanded_g)

        grad = tf.concat(grads, 0)
        grad = tf.reduce_mean(grad, 0)

        v = grad_and_vars[0][1]
        grad_and_var = (grad, v)
        average_grads.append(grad_and_var)

    return average_grads


def main(argv=None):
    import os

    os.environ['CUDA_VISIBLE_DEVICES'] = FLAGS.gpu_list
    if not tf.gfile.Exists(FLAGS.checkpoint_path):
        tf.gfile.MkDir(FLAGS.checkpoint_path)
    else:
        if not FLAGS.restore:
            tf.gfile.DeleteRecursively(FLAGS.checkpoint_path)
            tf.gfile.MkDir(FLAGS.checkpoint_path)

    input_images = tf.placeholder(tf.float32, shape=[None, None, None, 3], name='input_images')
    input_labels = tf.placeholder(tf.int32, shape=[None], name='input_labels')   #Fixme: for classification
    input_score_maps = tf.placeholder(tf.float32, shape=[None, None, None, 1], name='input_score_maps')
    if FLAGS.geometry == 'RBOX':
        input_geo_maps = tf.placeholder(tf.float32, shape=[None, None, None, 5], name='input_geo_maps')
    else:
        input_geo_maps = tf.placeholder(tf.float32, shape=[None, None, None, 8], name='input_geo_maps')
    input_training_masks = tf.placeholder(tf.float32, shape=[None, None, None, 1], name='input_training_masks')

    input_maps_KR = tf.placeholder(tf.float32, shape=[None, None, None, 4], name='input_maps_KR')
    input_score_maps_KR = tf.placeholder(tf.float32, shape=[None, None, None, 1], name='input_score_maps_KR')

    global_step = tf.get_variable('global_step', [], initializer=tf.constant_initializer(0), trainable=False)
    learning_rate = tf.train.exponential_decay(FLAGS.learning_rate, global_step, decay_steps=10000, decay_rate=0.94, staircase=True)
    # add summary
    tf.summary.scalar('learning_rate', learning_rate)
    opt = tf.train.AdamOptimizer(learning_rate)
    # opt = tf.train.MomentumOptimizer(learning_rate, 0.9)


    # split
    input_images_split = tf.split(input_images, len(gpus))
    input_labels_split = tf.split(input_labels, len(gpus))     # Fixme: for classification
    input_score_maps_split = tf.split(input_score_maps, len(gpus))
    input_geo_maps_split = tf.split(input_geo_maps, len(gpus))
    input_training_masks_split = tf.split(input_training_masks, len(gpus))

    # FIXME: For position-sensitive map
    input_ps_maps_split = tf.split(input_maps_KR, len(gpus))
    input_ps_score_maps_split = tf.split(input_score_maps_KR, len(gpus))

    tower_grads = []
    reuse_variables = None
    for i, gpu_id in enumerate(gpus):
        with tf.device('/gpu:%d' % gpu_id):
            with tf.name_scope('model_%d' % gpu_id) as scope:
                iis = input_images_split[i]
                ils = input_labels_split[i]    # Fixme: for classification
                isms = input_score_maps_split[i]
                igms = input_geo_maps_split[i]
                itms = input_training_masks_split[i]
                ipms = input_ps_maps_split[i]
                ipsms = input_ps_score_maps_split[i]
                total_loss, model_loss = tower_loss(iis, ils, isms, ipms, ipsms, igms, itms, reuse_variables)
                batch_norm_updates_op = tf.group(*tf.get_collection(tf.GraphKeys.UPDATE_OPS, scope))
                reuse_variables = True

                grads = opt.compute_gradients(total_loss)
                tower_grads.append(grads)

    grads = average_gradients(tower_grads)
    apply_gradient_op = opt.apply_gradients(grads, global_step=global_step)

    summary_op = tf.summary.merge_all()
    # save moving average
    variable_averages = tf.train.ExponentialMovingAverage(
        FLAGS.moving_average_decay, global_step)
    variables_averages_op = variable_averages.apply(tf.trainable_variables())
    # batch norm updates
    with tf.control_dependencies([variables_averages_op, apply_gradient_op, batch_norm_updates_op]):
        train_op = tf.no_op(name='train_op')

    saver = tf.train.Saver(tf.global_variables(),max_to_keep=0)
    summary_writer = tf.summary.FileWriter(FLAGS.checkpoint_path, tf.get_default_graph())

    init = tf.global_variables_initializer()

    if FLAGS.pretrained_model_path is not None:
        variable_restore_op = slim.assign_from_checkpoint_fn(FLAGS.pretrained_model_path, slim.get_trainable_variables(),
                                                             ignore_missing_vars=True)
    with tf.Session(config=tf.ConfigProto(allow_soft_placement=True)) as sess:
        if FLAGS.restore:
            print('continue training from previous checkpoint')
            ckpt = tf.train.latest_checkpoint(FLAGS.checkpoint_path)
            saver.restore(sess, ckpt)
        else:
            sess.run(init)
            if FLAGS.pretrained_model_path is not None:
                variable_restore_op(sess)

        data_generator = data.get_batch(num_workers=FLAGS.num_readers,
                                          input_size=FLAGS.input_size,
                                          batch_size=FLAGS.batch_size_per_gpu * len(gpus))
        # data_generator = data.generator(input_size=FLAGS.input_size, batch_size=FLAGS.batch_size_per_gpu * len(gpus))

        start = time.time()
        for step in range(FLAGS.max_steps):
            data = next(data_generator)
            ml, tl, _ = sess.run([model_loss, total_loss, train_op], feed_dict={input_images: data[0],
                                                                                input_score_maps: data[2],
                                                                                input_geo_maps: data[3],
                                                                                input_training_masks: data[4],
                                                                                input_maps_KR: data[5],
                                                                                input_score_maps_KR: data[6],
                                                                                input_labels: data[7]})
            if np.isnan(tl):
                print('Loss diverged, stop training')
                break

            if step % 10 == 0:
                avg_time_per_step = (time.time() - start)/10
                avg_examples_per_second = (10 * FLAGS.batch_size_per_gpu * len(gpus))/(time.time() - start)
                start = time.time()
                print('Step {:06d}, model loss {:.4f}, total loss {:.4f}, {:.2f} seconds/step, {:.2f} examples/second'.format(
                    step, ml, tl, avg_time_per_step, avg_examples_per_second))

            if step % FLAGS.save_checkpoint_steps == 0:
                # saver.save(sess, FLAGS.checkpoint_path + 'model.ckpt', global_step=global_step)            # for ubuntu
                saver.save(sess, os.path.join(FLAGS.checkpoint_path, 'model.ckpt'), global_step=global_step)  # for windows

            # if step % FLAGS.save_summary_steps == 0:
            #     _, tl, summary_str = sess.run([train_op, total_loss, summary_op], feed_dict={input_images: data[0],
            #                                                                                  input_score_maps: data[2],
            #                                                                                  input_geo_maps: data[3],
            #                                                                                  input_training_masks: data[4],
            #                                                                                  input_maps_KR: data[5],
            #                                                                                  input_score_maps_KR: data[6]})
            #     summary_writer.add_summary(summary_str, global_step=step)
        #fp_loss.close()

if __name__ == '__main__':
    tf.app.run()
