#! /usr/bin/python
# -*- coding: utf-8 -*-
"""YOLOv4 for MS-COCO.

# Reference:
- [tensorflow-yolov4-tflite](
    https://github.com/hunglc007/tensorflow-yolov4-tflite)

"""

import numpy as np
import tensorlayer as tl
from tensorlayer.layers.activation import Mish
from tensorlayer.layers import Conv2d, MaxPool2d, BatchNorm2d, ZeroPad2d, UpSampling2d, Concat, Elementwise
from tensorlayer.layers import Module, SequentialLayer
from tensorlayer import logging

__all__ = ['YOLOv4']

INPUT_SIZE = 416
weights_url = {'link': 'https://pan.baidu.com/s/1MC1dmEwpxsdgHO1MZ8fYRQ', 'password': 'idsz'}


class Convolutional(Module):
    """
    Create Convolution layer
    Because it is only a stack of reference layers, there is no build, so self._built=True
    """

    def __init__(self, filters_shape, downsample=False, activate=True, bn=True, activate_type='leaky', name=None):
        super(Convolutional, self).__init__()
        self.act = activate
        self.act_type = activate_type
        self.downsample = downsample
        self.bn = bn
        self._built = True
        if downsample:
            padding = 'VALID'
            strides = 2
        else:
            strides = 1
            padding = 'SAME'

        if bn:
            b_init = None
        else:
            b_init = tl.initializers.constant(value=0.0)

        self.zeropad = ZeroPad2d(((1, 0), (1, 0)))
        self.conv = Conv2d(
            n_filter=filters_shape[-1], in_channels=filters_shape[2], filter_size=(filters_shape[0], filters_shape[1]),
            strides=(strides, strides), padding=padding, b_init=b_init, name=name
        )

        if bn:
            if activate ==True:
                if activate_type == 'leaky':
                    self.batchnorm2d = BatchNorm2d(act='leaky_relu0.1', num_features=filters_shape[-1])
                elif activate_type == 'mish':
                    self.batchnorm2d = BatchNorm2d(act=Mish, num_features=filters_shape[-1])
            else:
                self.batchnorm2d = BatchNorm2d(act=None, num_features=filters_shape[-1])

    def forward(self, input):
        if self.downsample:
            input = self.zeropad(input)

        output = self.conv(input)

        if self.bn:
            output = self.batchnorm2d(output)
        return output


class residual_block(Module):

    def __init__(self, input_channel, filter_num1, filter_num2, activate_type='leaky'):
        super(residual_block, self).__init__()
        self.conv1 = Convolutional(filters_shape=(1, 1, input_channel, filter_num1), activate_type=activate_type)
        self.conv2 = Convolutional(filters_shape=(3, 3, filter_num1, filter_num2), activate_type=activate_type)
        self.add = Elementwise(tl.add)

    def forward(self, inputs):
        output = self.conv1(inputs)
        output = self.conv2(output)
        output = self.add([inputs, output])
        return output


def residual_block_num(num, input_channel, filter_num1, filter_num2, activate_type='leaky'):
    residual_list = []
    for i in range(num):
        residual_list.append(residual_block(input_channel, filter_num1, filter_num2, activate_type=activate_type))
    return SequentialLayer(residual_list)


class cspdarknet53(Module):

    def __init__(self):
        super(cspdarknet53, self).__init__()
        self._built = True
        self.conv1_1 = Convolutional((3, 3, 3, 32), activate_type='mish')
        self.conv1_2 = Convolutional((3, 3, 32, 64), downsample=True, activate_type='mish')
        self.conv1_3 = Convolutional((1, 1, 64, 64), activate_type='mish', name='conv_rote_block_1')
        self.conv1_4 = Convolutional((1, 1, 64, 64), activate_type='mish')
        self.residual_1 = residual_block_num(1, 64, 32, 64, activate_type="mish")

        self.conv2_1 = Convolutional((1, 1, 64, 64), activate_type='mish')
        self.concat = Concat()
        self.conv2_2 = Convolutional((1, 1, 128, 64), activate_type='mish')
        self.conv2_3 = Convolutional((3, 3, 64, 128), downsample=True, activate_type='mish')
        self.conv2_4 = Convolutional((1, 1, 128, 64), activate_type='mish', name='conv_rote_block_2')
        self.conv2_5 = Convolutional((1, 1, 128, 64), activate_type='mish')
        self.residual_2 = residual_block_num(2, 64, 64, 64, activate_type='mish')

        self.conv3_1 = Convolutional((1, 1, 64, 64), activate_type='mish')
        self.conv3_2 = Convolutional((1, 1, 128, 128), activate_type='mish')
        self.conv3_3 = Convolutional((3, 3, 128, 256), downsample=True, activate_type='mish')
        self.conv3_4 = Convolutional((1, 1, 256, 128), activate_type='mish', name='conv_rote_block_3')
        self.conv3_5 = Convolutional((1, 1, 256, 128), activate_type='mish')
        self.residual_3 = residual_block_num(8, 128, 128, 128, activate_type="mish")

        self.conv4_1 = Convolutional((1, 1, 128, 128), activate_type='mish')
        self.conv4_2 = Convolutional((1, 1, 256, 256), activate_type='mish')
        self.conv4_3 = Convolutional((3, 3, 256, 512), downsample=True, activate_type='mish')
        self.conv4_4 = Convolutional((1, 1, 512, 256), activate_type='mish', name='conv_rote_block_4')
        self.conv4_5 = Convolutional((1, 1, 512, 256), activate_type='mish')
        self.residual_4 = residual_block_num(8, 256, 256, 256, activate_type="mish")

        self.conv5_1 = Convolutional((1, 1, 256, 256), activate_type='mish')
        self.conv5_2 = Convolutional((1, 1, 512, 512), activate_type='mish')
        self.conv5_3 = Convolutional((3, 3, 512, 1024), downsample=True, activate_type='mish')
        self.conv5_4 = Convolutional((1, 1, 1024, 512), activate_type='mish', name='conv_rote_block_5')
        self.conv5_5 = Convolutional((1, 1, 1024, 512), activate_type='mish')
        self.residual_5 = residual_block_num(4, 512, 512, 512, activate_type="mish")

        self.conv6_1 = Convolutional((1, 1, 512, 512), activate_type='mish')
        self.conv6_2 = Convolutional((1, 1, 1024, 1024), activate_type='mish')
        self.conv6_3 = Convolutional((1, 1, 1024, 512))
        self.conv6_4 = Convolutional((3, 3, 512, 1024))
        self.conv6_5 = Convolutional((1, 1, 1024, 512))

        self.maxpool1 = MaxPool2d(filter_size=(13, 13), strides=(1, 1))
        self.maxpool2 = MaxPool2d(filter_size=(9, 9), strides=(1, 1))
        self.maxpool3 = MaxPool2d(filter_size=(5, 5), strides=(1, 1))

        self.conv7_1 = Convolutional((1, 1, 2048, 512))
        self.conv7_2 = Convolutional((3, 3, 512, 1024))
        self.conv7_3 = Convolutional((1, 1, 1024, 512))

    def forward(self, input_data):
        input_data = self.conv1_1(input_data)
        input_data = self.conv1_2(input_data)
        route = input_data
        route = self.conv1_3(route)
        input_data = self.conv1_4(input_data)
        input_data = self.residual_1(input_data)

        input_data = self.conv2_1(input_data)
        input_data = self.concat([input_data, route])
        input_data = self.conv2_2(input_data)
        input_data = self.conv2_3(input_data)
        route = input_data
        route = self.conv2_4(route)
        input_data = self.conv2_5(input_data)
        input_data = self.residual_2(input_data)

        input_data = self.conv3_1(input_data)
        input_data = self.concat([input_data, route])
        input_data = self.conv3_2(input_data)
        input_data = self.conv3_3(input_data)
        route = input_data
        route = self.conv3_4(route)
        input_data = self.conv3_5(input_data)
        input_data = self.residual_3(input_data)

        input_data = self.conv4_1(input_data)
        input_data = self.concat([input_data, route])
        input_data = self.conv4_2(input_data)
        route_1 = input_data
        input_data = self.conv4_3(input_data)
        route = input_data
        route = self.conv4_4(route)
        input_data = self.conv4_5(input_data)
        input_data = self.residual_4(input_data)

        input_data = self.conv5_1(input_data)
        input_data = self.concat([input_data, route])
        input_data = self.conv5_2(input_data)
        route_2 = input_data
        input_data = self.conv5_3(input_data)
        route = input_data
        route = self.conv5_4(route)
        input_data = self.conv5_5(input_data)
        input_data = self.residual_5(input_data)

        input_data = self.conv6_1(input_data)
        input_data = self.concat([input_data, route])

        input_data = self.conv6_2(input_data)
        input_data = self.conv6_3(input_data)
        input_data = self.conv6_4(input_data)
        input_data = self.conv6_5(input_data)

        maxpool1 = self.maxpool1(input_data)
        maxpool2 = self.maxpool2(input_data)
        maxpool3 = self.maxpool3(input_data)
        input_data = self.concat([maxpool1, maxpool2, maxpool3, input_data])

        input_data = self.conv7_1(input_data)
        input_data = self.conv7_2(input_data)
        input_data = self.conv7_3(input_data)

        return route_1, route_2, input_data


class YOLOv4_model(Module):

    def __init__(self, NUM_CLASS):
        super(YOLOv4_model, self).__init__()
        self.cspdarnnet = cspdarknet53()

        self.conv1_1 = Convolutional((1, 1, 512, 256))
        self.upsamle = UpSampling2d(scale=2)
        self.conv1_2 = Convolutional((1, 1, 512, 256), name='conv_yolo_1')
        self.concat = Concat()

        self.conv2_1 = Convolutional((1, 1, 512, 256))
        self.conv2_2 = Convolutional((3, 3, 256, 512))
        self.conv2_3 = Convolutional((1, 1, 512, 256))
        self.conv2_4 = Convolutional((3, 3, 256, 512))
        self.conv2_5 = Convolutional((1, 1, 512, 256))

        self.conv3_1 = Convolutional((1, 1, 256, 128))
        self.conv3_2 = Convolutional((1, 1, 256, 128), name='conv_yolo_2')

        self.conv4_1 = Convolutional((1, 1, 256, 128))
        self.conv4_2 = Convolutional((3, 3, 128, 256))
        self.conv4_3 = Convolutional((1, 1, 256, 128))
        self.conv4_4 = Convolutional((3, 3, 128, 256))
        self.conv4_5 = Convolutional((1, 1, 256, 128))

        self.conv5_1 = Convolutional((3, 3, 128, 256), name='conv_route_1')
        self.conv5_2 = Convolutional((1, 1, 256, 3 * (NUM_CLASS + 5)), activate=False, bn=False)

        self.conv6_1 = Convolutional((3, 3, 128, 256), downsample=True, name='conv_route_2')
        self.conv6_2 = Convolutional((1, 1, 512, 256))
        self.conv6_3 = Convolutional((3, 3, 256, 512))
        self.conv6_4 = Convolutional((1, 1, 512, 256))
        self.conv6_5 = Convolutional((3, 3, 256, 512))
        self.conv6_6 = Convolutional((1, 1, 512, 256))

        self.conv7_1 = Convolutional((3, 3, 256, 512), name='conv_route_3')
        self.conv7_2 = Convolutional((1, 1, 512, 3 * (NUM_CLASS + 5)), activate=False, bn=False)
        self.conv7_3 = Convolutional((3, 3, 256, 512), downsample=True, name='conv_route_4')

        self.conv8_1 = Convolutional((1, 1, 1024, 512))
        self.conv8_2 = Convolutional((3, 3, 512, 1024))
        self.conv8_3 = Convolutional((1, 1, 1024, 512))
        self.conv8_4 = Convolutional((3, 3, 512, 1024))
        self.conv8_5 = Convolutional((1, 1, 1024, 512))

        self.conv9_1 = Convolutional((3, 3, 512, 1024))
        self.conv9_2 = Convolutional((1, 1, 1024, 3 * (NUM_CLASS + 5)), activate=False, bn=False)

    def forward(self, inputs):
        route_1, route_2, conv = self.cspdarnnet(inputs)

        route = conv
        conv = self.conv1_1(conv)
        conv = self.upsamle(conv)
        route_2 = self.conv1_2(route_2)
        conv = self.concat([route_2, conv])

        conv = self.conv2_1(conv)
        conv = self.conv2_2(conv)
        conv = self.conv2_3(conv)
        conv = self.conv2_4(conv)
        conv = self.conv2_5(conv)

        route_2 = conv
        conv = self.conv3_1(conv)
        conv = self.upsamle(conv)
        route_1 = self.conv3_2(route_1)
        conv = self.concat([route_1, conv])

        conv = self.conv4_1(conv)
        conv = self.conv4_2(conv)
        conv = self.conv4_3(conv)
        conv = self.conv4_4(conv)
        conv = self.conv4_5(conv)

        route_1 = conv
        conv = self.conv5_1(conv)
        conv_sbbox = self.conv5_2(conv)

        conv = self.conv6_1(route_1)
        conv = self.concat([conv, route_2])

        conv = self.conv6_2(conv)
        conv = self.conv6_3(conv)
        conv = self.conv6_4(conv)
        conv = self.conv6_5(conv)
        conv = self.conv6_6(conv)

        route_2 = conv
        conv = self.conv7_1(conv)
        conv_mbbox = self.conv7_2(conv)
        conv = self.conv7_3(route_2)
        conv = self.concat([conv, route])

        conv = self.conv8_1(conv)
        conv = self.conv8_2(conv)
        conv = self.conv8_3(conv)
        conv = self.conv8_4(conv)
        conv = self.conv8_5(conv)

        conv = self.conv9_1(conv)
        conv_lbbox = self.conv9_2(conv)

        return conv_sbbox, conv_mbbox, conv_lbbox


def YOLOv4(NUM_CLASS, pretrained=False):
    """Pre-trained YOLOv4 model.

    Parameters
    ------------
    NUM_CLASS : int
        Number of classes in final prediction.
    pretrained : boolean
        Whether to load pretrained weights. Default False.

    Examples
    ---------
    Object Detection with YOLOv4, see `computer_vision.py
    <https://github.com/tensorlayer/tensorlayer/blob/master/tensorlayer/app/computer_vision.py>`__
    With TensorLayer

    >>> # get the whole model, without pre-trained YOLOv4 parameters
    >>> yolov4 = YOLOv4(NUM_CLASS=80, pretrained=False)
    >>> # get the whole model, restore pre-trained YOLOv4 parameters
    >>> yolov4 = YOLOv4(NUM_CLASS=80, pretrained=True)
    >>> # use for inferencing
    >>> output = yolov4(img)

    """

    network = YOLOv4_model(NUM_CLASS=NUM_CLASS)

    if pretrained:
        restore_params(network, model_path='model/yolov4_model.npz')

    return network


def restore_params(network, model_path='models.npz'):
    logging.info("Restore pre-trained weights")

    try:
        npz = np.load(model_path, allow_pickle=True)
    except:
        print("Download the model file, placed in the /model ")
        print("Weights download: ", weights_url['link'], "password:", weights_url['password'])

    txt_path = 'model/yolov4_weights3_config.txt'
    f = open(txt_path, "r")
    line = f.readlines()
    for i in range(len(line)):
        network.all_weights[i].assign(npz[line[i].strip()])
        logging.info("  Loading weights %s in %s" % (network.all_weights[i].shape, network.all_weights[i].name))


def tl2_weights_to_tl3_weights(
    weights_2_path='model/weights_2.txt', weights_3_path='model/weights_3.txt',
    txt_path='model/yolov4_weights_config.txt'
):
    weights_2_path = weights_2_path
    weights_3_path = weights_3_path
    txt_path = txt_path
    f1 = open(weights_2_path, "r")
    f2 = open(weights_3_path, "r")
    f3 = open(txt_path, "r")
    line1 = f1.readlines()
    line2 = f2.readlines()
    line3 = f3.readlines()
    _dicts = {}
    for i in range(len(line1)):
        _dicts[line1[i].strip()] = line3[i].strip()
    for j in range(len(line2)):
        print(_dicts[line2[j].strip()])
