#copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve.
#
#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 paddle.fluid as fluid
from paddle.fluid.initializer import MSRA
from paddle.fluid.param_attr import ParamAttr

__all__ = [
    'SlimMobileNet_v1', 'SlimMobileNet_v2', 'SlimMobileNet_v3',
    'SlimMobileNet_v4', 'SlimMobileNet_v5'
]


class SlimMobileNet():
    def __init__(self, scale=1.0, model_name='large', token=[]):
        assert len(token) >= 45
        self.kernel_size_lis = token[:20]
        self.exp_lis = token[20:40]
        self.depth_lis = token[40:45]

        self.scale = scale
        self.inplanes = 16
        if model_name == "large":
            self.cfg_channel = [16, 24, 40, 80, 112, 160]
            self.cfg_stride = [1, 2, 2, 2, 1, 2]
            self.cfg_se = [False, False, True, False, True, True]
            self.cfg_act = [
                'relu', 'relu', 'relu', 'hard_swish', 'hard_swish',
                'hard_swish'
            ]
            self.cls_ch_squeeze = 960
            self.cls_ch_expand = 1280
        else:
            raise NotImplementedError("mode[" + model_name +
                                      "_model] is not implemented!")

    def net(self, input, class_dim=1000):
        scale = self.scale
        inplanes = self.inplanes

        kernel_size_lis = self.kernel_size_lis
        exp_lis = self.exp_lis
        depth_lis = self.depth_lis
        cfg_channel = self.cfg_channel
        cfg_stride = self.cfg_stride
        cfg_se = self.cfg_se
        cfg_act = self.cfg_act

        cls_ch_squeeze = self.cls_ch_squeeze
        cls_ch_expand = self.cls_ch_expand
        #conv1
        conv = self.conv_bn_layer(
            input,
            filter_size=3,
            num_filters=self.make_divisible(inplanes * scale),
            stride=2,
            padding=1,
            num_groups=1,
            if_act=True,
            act='hard_swish',
            name='conv1')
        inplanes = self.make_divisible(inplanes * scale)

        #conv2
        num_mid_filter = self.make_divisible(scale * inplanes)
        _num_out_filter = cfg_channel[0]
        num_out_filter = self.make_divisible(scale * _num_out_filter)
        conv = self.residual_unit(
            input=conv,
            num_in_filter=inplanes,
            num_mid_filter=num_mid_filter,
            num_out_filter=num_out_filter,
            act=cfg_act[0],
            stride=cfg_stride[0],
            filter_size=3,
            use_se=cfg_se[0],
            name='conv2',
            short=True)
        inplanes = self.make_divisible(scale * cfg_channel[0])

        i = 3
        for depth_id in range(len(depth_lis)):
            for repeat_time in range(depth_lis[depth_id]):
                num_mid_filter = self.make_divisible(
                    scale * _num_out_filter *
                    exp_lis[depth_id * 4 + repeat_time])
                _num_out_filter = cfg_channel[depth_id + 1]
                num_out_filter = self.make_divisible(scale * _num_out_filter)
                stride = cfg_stride[depth_id + 1] if repeat_time == 0 else 1
                conv = self.residual_unit(
                    input=conv,
                    num_in_filter=inplanes,
                    num_mid_filter=num_mid_filter,
                    num_out_filter=num_out_filter,
                    act=cfg_act[depth_id + 1],
                    stride=stride,
                    filter_size=kernel_size_lis[depth_id * 4 + repeat_time],
                    use_se=cfg_se[depth_id + 1],
                    name='conv' + str(i))

                inplanes = self.make_divisible(scale *
                                               cfg_channel[depth_id + 1])
                i += 1

        conv = self.conv_bn_layer(
            input=conv,
            filter_size=1,
            num_filters=self.make_divisible(scale * cls_ch_squeeze),
            stride=1,
            padding=0,
            num_groups=1,
            if_act=True,
            act='hard_swish',
            name='conv_last')
        conv = fluid.layers.pool2d(
            input=conv, pool_type='avg', global_pooling=True, use_cudnn=False)
        conv = fluid.layers.conv2d(
            input=conv,
            num_filters=cls_ch_expand,
            filter_size=1,
            stride=1,
            padding=0,
            act=None,
            param_attr=ParamAttr(name='last_1x1_conv_weights'),
            bias_attr=False)
        conv = fluid.layers.hard_swish(conv)
        drop = fluid.layers.dropout(x=conv, dropout_prob=0.2)
        out = fluid.layers.fc(input=drop,
                              size=class_dim,
                              param_attr=ParamAttr(name='fc_weights'),
                              bias_attr=ParamAttr(name='fc_offset'))
        return out

    def conv_bn_layer(self,
                      input,
                      filter_size,
                      num_filters,
                      stride,
                      padding,
                      num_groups=1,
                      if_act=True,
                      act=None,
                      name=None,
                      use_cudnn=True,
                      res_last_bn_init=False):
        conv = fluid.layers.conv2d(
            input=input,
            num_filters=num_filters,
            filter_size=filter_size,
            stride=stride,
            padding=padding,
            groups=num_groups,
            act=None,
            use_cudnn=use_cudnn,
            param_attr=ParamAttr(name=name + '_weights'),
            bias_attr=False)
        bn_name = name + '_bn'
        bn = fluid.layers.batch_norm(
            input=conv,
            param_attr=ParamAttr(
                name=bn_name + "_scale",
                regularizer=fluid.regularizer.L2DecayRegularizer(
                    regularization_coeff=0.0)),
            bias_attr=ParamAttr(
                name=bn_name + "_offset",
                regularizer=fluid.regularizer.L2DecayRegularizer(
                    regularization_coeff=0.0)),
            moving_mean_name=bn_name + '_mean',
            moving_variance_name=bn_name + '_variance')
        if if_act:
            if act == 'relu':
                bn = fluid.layers.relu(bn)
            elif act == 'hard_swish':
                bn = fluid.layers.hard_swish(bn)
        return bn

    def make_divisible(self, v, divisor=8, min_value=None):
        if min_value is None:
            min_value = divisor
        new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
        if new_v < 0.9 * v:
            new_v += divisor
        return new_v

    def se_block(self, input, num_out_filter, ratio=4, name=None):
        num_mid_filter = num_out_filter // ratio
        pool = fluid.layers.pool2d(
            input=input, pool_type='avg', global_pooling=True, use_cudnn=False)
        conv1 = fluid.layers.conv2d(
            input=pool,
            filter_size=1,
            num_filters=num_mid_filter,
            act='relu',
            param_attr=ParamAttr(name=name + '_1_weights'),
            bias_attr=ParamAttr(name=name + '_1_offset'))
        conv2 = fluid.layers.conv2d(
            input=conv1,
            filter_size=1,
            num_filters=num_out_filter,
            act='hard_sigmoid',
            param_attr=ParamAttr(name=name + '_2_weights'),
            bias_attr=ParamAttr(name=name + '_2_offset'))
        scale = fluid.layers.elementwise_mul(x=input, y=conv2, axis=0)
        return scale

    def residual_unit(self,
                      input,
                      num_in_filter,
                      num_mid_filter,
                      num_out_filter,
                      stride,
                      filter_size,
                      act=None,
                      use_se=False,
                      name=None,
                      short=False):

        if not short:
            conv0 = self.conv_bn_layer(
                input=input,
                filter_size=1,
                num_filters=num_mid_filter,
                stride=1,
                padding=0,
                if_act=True,
                act=act,
                name=name + '_expand')
        else:
            conv0 = input

        conv1 = self.conv_bn_layer(
            input=conv0,
            filter_size=filter_size,
            num_filters=num_mid_filter,
            stride=stride,
            padding=int((filter_size - 1) // 2),
            if_act=True,
            act=act,
            num_groups=num_mid_filter,
            use_cudnn=False,
            name=name + '_depthwise')
        if use_se:
            conv1 = self.se_block(
                input=conv1, num_out_filter=num_mid_filter, name=name + '_se')

        conv2 = self.conv_bn_layer(
            input=conv1,
            filter_size=1,
            num_filters=num_out_filter,
            stride=1,
            padding=0,
            if_act=False,
            name=name + '_linear',
            res_last_bn_init=True)
        if num_in_filter != num_out_filter or stride != 1:
            return conv2
        else:
            return fluid.layers.elementwise_add(x=input, y=conv2, act=None)


def SlimMobileNet_v1(token):
    token = [
        5, 3, 3, 7, 3, 3, 5, 7, 3, 3, 3, 3, 3, 3, 7, 3, 5, 3, 3, 3, 3, 3, 3, 6,
        3, 3, 3, 3, 4, 4, 4, 6, 4, 3, 4, 3, 6, 4, 3, 3, 2, 2, 2, 2, 4
    ]
    model = SlimMobileNet(model_name='large', scale=1.0, token=token)
    return model


def SlimMobileNet_v2(token):
    token = [
        5, 3, 5, 7, 3, 3, 7, 3, 5, 3, 3, 7, 3, 3, 3, 5, 5, 5, 3, 3, 3, 3, 4, 6,
        3, 3, 6, 3, 4, 4, 3, 4, 4, 4, 3, 6, 6, 4, 3, 3, 2, 2, 3, 2, 4
    ]
    model = SlimMobileNet(model_name='large', scale=1.0, token=token)
    return model


def SlimMobileNet_v3(token):
    token = [
        3, 3, 3, 3, 5, 3, 7, 7, 7, 3, 3, 7, 5, 3, 5, 7, 5, 3, 3, 3, 3, 3, 3, 3,
        3, 4, 3, 4, 3, 6, 4, 4, 4, 4, 6, 3, 6, 4, 6, 3, 2, 2, 3, 2, 4
    ]
    model = SlimMobileNet(model_name='large', scale=1.0, token=token)
    return model


def SlimMobileNet_v4(token):
    token = [
        3, 3, 3, 3, 5, 3, 3, 5, 7, 3, 5, 5, 5, 3, 3, 7, 3, 5, 3, 3, 3, 3, 4, 6,
        3, 4, 4, 6, 4, 6, 4, 6, 4, 6, 4, 4, 6, 6, 6, 4, 2, 3, 3, 3, 4
    ]
    model = SlimMobileNet(model_name='large', scale=1.0, token=token)
    return model


def SlimMobileNet_v5(token):
    token = [
        7, 7, 3, 5, 7, 3, 5, 3, 7, 5, 3, 3, 5, 3, 7, 5, 7, 7, 5, 3, 3, 3, 6, 3,
        4, 6, 3, 6, 6, 3, 6, 4, 6, 6, 4, 3, 6, 6, 6, 6, 4, 4, 4, 4, 4
    ]
    model = SlimMobileNet(model_name='large', scale=1.0, token=token)
    return model


if __name__ == "__main__":
    pass
