# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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

import math
import six
import numpy as np
from paddle import fluid
from paddle.fluid.param_attr import ParamAttr
from paddle.fluid.regularizer import L2Decay

from ppdet.core.workspace import register

__all__ = ['CSPYolo']


def autopad(k, p):
    if p is None:
        p = k // 2 if isinstance(k, int) else [x // 2 for x in k]  # auto-pad
    return p


def make_divisible(x, divisor):
    # Returns x evenly divisble by divisor
    return math.ceil(x / divisor) * divisor


@register
class CSPYolo(object):

    __shared__ = ['depth_multiple', 'width_multiple']

    def __init__(self,
                 layers=None,
                 neck=None,
                 depth_multiple=0.33,
                 width_multiple=0.50,
                 act='none',
                 yolov5=True,
                 save=[17, 20, 23],
                 conv_decay=0.0,
                 norm_type='bn',
                 norm_decay=0.0,
                 weight_prefix_name=''):

        if layers is None:
            self.layers = [
                # [from, number, module, args, kwargs]
                [-1, 1, 'Focus', [64, 3]],  # 0-P1/2
                [-1, 1, 'Conv', [128, 3, 2]],  # 1-P2/4
                [-1, 3, 'BottleneckCSP', [128]],
                [-1, 1, 'Conv', [256, 3, 2]],  # 3-P3/8
                [-1, 9, 'BottleneckCSP', [256]],
                [-1, 1, 'Conv', [512, 3, 2]],  # 5-P4/16
                [-1, 9, 'BottleneckCSP', [512]],
                [-1, 1, 'Conv', [1024, 3, 2]],  # 7-P5/32
                [-1, 1, 'SPP', [1024, [5, 9, 13]]],
                [-1, 3, 'BottleneckCSP', [1024, False]],  # 9
            ]
        else:
            self.layers = layers

        if neck is None:
            self.neck = [
                [-1, 1, 'Conv', [512, 1, 1]],
                [-1, 1, 'Upsample', [None, 2, 'nearest']],
                [[-1, 6], 1, 'Concat', [1]],  # cat backbone P4
                [-1, 3, 'BottleneckCSP', [512, False]],  # 13
                [-1, 1, 'Conv', [256, 1, 1]],
                [-1, 1, 'Upsample', [None, 2, 'nearest']],
                [[-1, 4], 1, 'Concat', [1]],  # cat backbone P3
                [-1, 3, 'BottleneckCSP', [256, False]],  # 17 (P3/8-small)
                [-1, 1, 'Conv', [256, 3, 2]],
                [[-1, 14], 1, 'Concat', [1]],  # cat head P4
                [-1, 3, 'BottleneckCSP', [512, False]],  # 20 (P4/16-medium)
                [-1, 1, 'Conv', [512, 3, 2]],
                [[-1, 10], 1, 'Concat', [1]],  # cat head P5
                [-1, 3, 'BottleneckCSP', [1024, False]],  # 23 (P5/32-large)
            ]
        else:
            self.neck = neck

        self.depth_multiple = depth_multiple
        self.width_multiple = width_multiple
        self.act = act
        self.yolov5 = yolov5
        self.save = save
        self.conv_decay = conv_decay
        self.norm_type = norm_type
        self.norm_decay = norm_decay
        self.weight_prefix_name = weight_prefix_name
        self.layer_cfg = {
            'Conv': self._conv,
            'Focus': self._focus,
            'Bottleneck': self._bottleneck,
            'BottleneckCSP': self._bottleneckcsp,
            'BottleneckCSP2': self._bottleneckcsp2,
            'SPP': self._spp,
            'SPPCSP': self._sppcsp,
            'Upsample': self._upsample,
            'Concat': self._concat
        }
        self.act_cfg = {
            'relu': fluid.layers.relu,
            'leaky_relu': lambda x: fluid.layers.leaky_relu(x, alpha=0.1),
            'hard_swish': self._hard_swish,
            'mish': self._mish,
            'none': self._identity
        }

    def _identity(self, x):
        return x

    def _hard_swish(self, x):
        return x * fluid.layers.relu6(x + 3) / 6.

    def _softplus(self, x):
        expf = fluid.layers.exp(fluid.layers.clip(x, -200, 50))
        return fluid.layers.log(1 + expf)

    def _mish(self, x):
        return x * fluid.layers.tanh(self._softplus(x))

    def _conv(self, x, c_out, k=1, s=1, p=None, g=1, act='none', name=None): 
        x = fluid.layers.conv2d(x,
                                c_out,
                                k,
                                stride=s,
                                padding=autopad(k, p),
                                groups=g,
                                param_attr=ParamAttr(
                                    regularizer=L2Decay(self.conv_decay),
                                    name=name + '.conv.weight'),
                                bias_attr=False)
        x = self._bn(x, name=name)
        x = self.act_cfg[act](x)
        return x

    def _bn(self, x, name=None):
        param_attr = ParamAttr(regularizer=L2Decay(self.norm_decay),
                               name=name + '.{}.weight'.format(self.norm_type))
        bias_attr = ParamAttr(regularizer=L2Decay(self.norm_decay),
                              name=name + '.{}.bias'.format(self.norm_type))

        x = fluid.layers.batch_norm(
            input=x,
            epsilon=0.001,
            param_attr=param_attr,
            bias_attr=bias_attr,
            moving_mean_name=name + '.{}.running_mean'.format(self.norm_type),
            moving_variance_name=name + '.{}.running_var'.format(self.norm_type))

        return x

    def _focus(self, x, c_out, k=1, s=1, p=None, g=1, act='none', name=None):
        x = fluid.layers.concat([
            x[:, :, 0::2, 0::2], x[:, :, 1::2, 0::2], x[:, :, 0::2, 1::2],
            x[:, :, 1::2, 1::2]
        ],
                                axis=1)
        x = self._conv(x, c_out, k, s, p, g, act, name + '.conv')
        return x

    def _bottleneck(self,
                    x,
                    c_out,
                    shortcut=True,
                    g=1,
                    e=0.5,
                    act='none',
                    name=None):
        c_h = int(c_out * e)
        y = self._conv(x, c_h, 1, 1, act=act, name=name + '.cv1')
        y = self._conv(y, c_out, 3, 1, g=g, act=act, name=name + '.cv2')
        if shortcut:
            y = fluid.layers.elementwise_add(x=x, y=y, act=None)
        return y

    def _bottleneckcsp(self,
                       x,
                       c_out,
                       n=1,
                       shortcut=True,
                       g=1,
                       e=0.5,
                       act='none',
                       name=None):
        c_h = int(c_out * e)
        # left branch
        
        y1 = self._conv(x, c_h, 1, 1, act=act, name=name + '.cv1')
        # n bottle neck
        bottleneck = self._bottleneck
        for i in six.moves.xrange(n):
            y1 = bottleneck(y1, c_h, shortcut, g, 1.0, act,
                                  name + '.m.{}'.format(i))
        y1 = fluid.layers.conv2d(
            y1,
            c_h,
            1,
            1,
            param_attr=ParamAttr(regularizer=L2Decay(self.conv_decay),
                                 name=name +
                                 '.cv3.weight'),
            bias_attr=False)
        # right branch
        y2 = fluid.layers.conv2d(x,
                                 c_h,
                                 1,
                                 1,
                                 param_attr=ParamAttr(
                                     regularizer=L2Decay(self.conv_decay),
                                     name=name + '.cv2.weight'),
                                 bias_attr=False)
        # concat
        y = fluid.layers.concat([y1, y2], axis=1)
        # bn + act
        y = self._bn(y, name=name)
        y = self.act_cfg['leaky_relu'](y) if self.yolov5 else self.act_cfg[act](
            y)
        # conv
        y = self._conv(y, c_out, 1, 1, act=act, name=name + '.cv4')
        return y

    def _bottleneckcsp2(self,
                        x,
                        c_out,
                        n=1,
                        shortcut=False,
                        g=1,
                        e=1.0,
                        act='none',
                        name=None):
        c_h = int(c_out)
        x = self._conv(x, c_h, 1, 1, act=act, name=name + '.cv1')
        # left_branch
        y1 = x
        for i in range(n):
            y1 = self._bottleneck(y1, c_h, shortcut, g, 1.0, act,
                                  name + '.m.{}'.format(i))
        # right_branch
        y2 = fluid.layers.conv2d(x,
                                 c_h,
                                 1,
                                 1,
                                 param_attr=ParamAttr(
                                     regularizer=L2Decay(self.conv_decay),
                                     name=name + '.cv2.weight'),
                                 bias_attr=False)
        # concat
        y = fluid.layers.concat([y1, y2], axis=1)
        # bn + act
        y = self._bn(y, name=name)
        y = self.act_cfg[act](y)
        # conv
        y = self._conv(y, c_out, 1, 1, act=act, name=name + '.cv3')
        return y

    def _spp(self, x, c_out, k=(5, 9, 13), act='none', name=None):
        c_in = int(x.shape[1])
        c_h = c_in // 2
        # conv1
        x = self._conv(x, c_h, 1, 1, act=act, name=name + '.cv1')
        ys = [x]
        # pooling
        for s in k:
            ys.append(fluid.layers.pool2d(x, s, 'max', 1, s // 2))
        y = fluid.layers.concat(ys, axis=1)
        # conv2
        y = self._conv(y, c_out, 1, 1, act=act, name=name + '.cv2')
        return y

    def _sppcsp(self,
                x,
                c_out,
                k=(5, 9, 13),
                e=0.5,
                act='none',
                name=None):
        c_h = int(2 * c_out * e)
        # left branch
        y1 = self._conv(x, c_h, 1, 1, act=act, name=name + '.cv1')
        y1 = self._conv(y1, c_h, 3, 1, act=act, name=name + '.cv3')
        y1 = self._conv(y1, c_h, 1, 1, act=act, name=name + '.cv4')
        ys = [y1]
        # pooling
        for s in k:
            ys.append(fluid.layers.pool2d(y1, s, 'max', 1, s // 2))
        y1 = fluid.layers.concat(ys, axis=1)

        y1 = self._conv(y1, c_h, 1, 1, act=act, name=name + '.cv5')
        y1 = self._conv(y1, c_h, 3, 1, act=act, name=name + '.cv6')
        # right_branch
        y2 = fluid.layers.conv2d(x,
                                 c_h,
                                 1,
                                 1,
                                 param_attr=ParamAttr(
                                     regularizer=L2Decay(self.conv_decay),
                                     name=name + '.cv2.weight'),
                                 bias_attr=False)
        # concat
        y = fluid.layers.concat([y1, y2], axis=1)
        y = self._bn(y, name=name)
        y = self.act_cfg[act](y)
        y = self._conv(y, c_out, 1, 1, act=act, name=name + '.cv7')
        return y

    def _upsample(self, x, out_shape, scale, method, name=None):
        out_shape = None if out_shape == 'None' else out_shape
        if name == 'bilinear':
            return fluid.layers.resize_bilinear(x, out_shape, scale, name=name)
        if name == 'trilinear':
            return fluid.layers.resize_trilinear(x, out_shape, scale, name=name)
        return fluid.layers.resize_nearest(x, out_shape, scale, name=name)

    def _concat(self, x, axis, name=None):
        y = fluid.layers.concat(x, axis, name=name)
        return y

    def Print(self, x):
        fluid.layers.Print(fluid.layers.reduce_max(x))
        fluid.layers.Print(fluid.layers.reduce_min(x))
        fluid.layers.Print(fluid.layers.reduce_mean(x))
        fluid.layers.Print(fluid.layers.reduce_mean(fluid.layers.abs(x)))
        
    def __call__(self, x):
        prefix = self.weight_prefix_name
        gw, gd = self.width_multiple, self.depth_multiple
        layers, outputs = [], []
        for i, (f, n, m, args) in enumerate(self.layers + self.neck):
            if i == 0:
                inputs = x
            else:
                if isinstance(f, int):
                    inputs = layers[f]
                else:
                    inputs = [layers[idx] for idx in f]
            n = max(round(n * gd), 1) if n > 1 else n
            if m in [
                    'Conv', 'Bottleneck', 'BottleneckCSP', 'BottleneckCSP2',
                    'SPP', 'SPPCSP', 'Focus'
            ]:
                c_out = args[0]
                args[0] = make_divisible(c_out * gw, 8)
                if m in ['BottleneckCSP', 'BottleneckCSP2']:
                    args.insert(1, n)

            if m in ['Upsample', 'Concat']:
                layers.append(self.layer_cfg[m](inputs,
                                            *args,
                                            name=prefix + '.{}'.format(i)))
            else:
                layers.append(self.layer_cfg[m](inputs,
                                            *args,
                                            act=self.act,
                                            name=prefix + '.{}'.format(i)))
            if i in self.save:
                outputs.append(layers[i])
        return outputs
