# Copyright (c) 2021 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.

import paddle
import paddle.nn as nn
import paddle.nn.functional as F

from paddlex.paddleseg.cvlibs import manager, param_init
from paddlex.paddleseg.models import layers
from paddlex.paddleseg.utils import utils

__all__ = ['ShuffleNetV2']


@manager.MODELS.add_component
class ShuffleNetV2(nn.Layer):
    def __init__(self, num_classes, pretrained=None, align_corners=False):
        super().__init__()
        self.pretrained = pretrained
        self.num_classes = num_classes
        self.align_corners = align_corners

        self.conv_bn0 = _ConvBNReLU(3, 36, 3, 2, 1)
        self.conv_bn1 = _ConvBNReLU(36, 18, 1, 1, 0)

        self.block1 = nn.Sequential(
            SFNetV2Module(
                36, stride=2, out_channels=72),
            SFNetV2Module(
                72, stride=1),
            SFNetV2Module(
                72, stride=1),
            SFNetV2Module(
                72, stride=1))

        self.block2 = nn.Sequential(
            SFNetV2Module(
                72, stride=2),
            SFNetV2Module(
                144, stride=1),
            SFNetV2Module(
                144, stride=1),
            SFNetV2Module(
                144, stride=1),
            SFNetV2Module(
                144, stride=1),
            SFNetV2Module(
                144, stride=1),
            SFNetV2Module(
                144, stride=1),
            SFNetV2Module(
                144, stride=1))

        self.depthwise_separable0 = _SeparableConvBNReLU(144, 64, 3, stride=1)
        self.depthwise_separable1 = _SeparableConvBNReLU(82, 64, 3, stride=1)

        weight_attr = paddle.ParamAttr(
            learning_rate=1.,
            regularizer=paddle.regularizer.L2Decay(coeff=0.),
            initializer=nn.initializer.XavierUniform())
        self.deconv = nn.Conv2DTranspose(
            64,
            self.num_classes,
            2,
            stride=2,
            padding=0,
            weight_attr=weight_attr,
            bias_attr=True)

        self.init_weight()

    def forward(self, x):
        ## Encoder
        conv1 = self.conv_bn0(x)  # encoder 1
        shortcut = self.conv_bn1(conv1)  # shortcut 1

        pool = F.max_pool2d(
            conv1, kernel_size=3, stride=2, padding=1)  # encoder 2

        # Block 1
        conv = self.block1(pool)  # encoder 3

        # Block 2
        conv = self.block2(conv)  # encoder 4

        ### decoder
        conv = self.depthwise_separable0(conv)
        shortcut_shape = paddle.shape(shortcut)[2:]
        conv_b = F.interpolate(
            conv,
            shortcut_shape,
            mode='bilinear',
            align_corners=self.align_corners)
        concat = paddle.concat(x=[shortcut, conv_b], axis=1)
        decode_conv = self.depthwise_separable1(concat)
        logit = self.deconv(decode_conv)
        return [logit]

    def init_weight(self):
        for layer in self.sublayers():
            if isinstance(layer, nn.Conv2D):
                param_init.normal_init(layer.weight, std=0.001)
            elif isinstance(layer, (nn.BatchNorm, nn.SyncBatchNorm)):
                param_init.constant_init(layer.weight, value=1.0)
                param_init.constant_init(layer.bias, value=0.0)
        if self.pretrained is not None:
            utils.load_pretrained_model(self, self.pretrained)


class _ConvBNReLU(nn.Layer):
    def __init__(self,
                 in_channels,
                 out_channels,
                 kernel_size,
                 stride,
                 padding,
                 groups=1,
                 **kwargs):
        super().__init__()
        weight_attr = paddle.ParamAttr(
            learning_rate=1, initializer=nn.initializer.KaimingUniform())
        self._conv = nn.Conv2D(
            in_channels,
            out_channels,
            kernel_size,
            padding=padding,
            stride=stride,
            groups=groups,
            weight_attr=weight_attr,
            bias_attr=False,
            **kwargs)

        self._batch_norm = layers.SyncBatchNorm(out_channels)

    def forward(self, x):
        x = self._conv(x)
        x = self._batch_norm(x)
        x = F.relu(x)
        return x


class _ConvBN(nn.Layer):
    def __init__(self,
                 in_channels,
                 out_channels,
                 kernel_size,
                 stride,
                 padding,
                 groups=1,
                 **kwargs):
        super().__init__()
        weight_attr = paddle.ParamAttr(
            learning_rate=1, initializer=nn.initializer.KaimingUniform())
        self._conv = nn.Conv2D(
            in_channels,
            out_channels,
            kernel_size,
            padding=padding,
            stride=stride,
            groups=groups,
            weight_attr=weight_attr,
            bias_attr=False,
            **kwargs)

        self._batch_norm = layers.SyncBatchNorm(out_channels)

    def forward(self, x):
        x = self._conv(x)
        x = self._batch_norm(x)
        return x


class _SeparableConvBNReLU(nn.Layer):
    def __init__(self, in_channels, out_channels, kernel_size, **kwargs):
        super().__init__()
        self.depthwise_conv = _ConvBN(
            in_channels,
            out_channels=in_channels,
            kernel_size=kernel_size,
            padding=int(kernel_size / 2),
            groups=in_channels,
            **kwargs)
        self.piontwise_conv = _ConvBNReLU(
            in_channels,
            out_channels,
            kernel_size=1,
            groups=1,
            stride=1,
            padding=0)

    def forward(self, x):
        x = self.depthwise_conv(x)
        x = self.piontwise_conv(x)
        return x


class SFNetV2Module(nn.Layer):
    def __init__(self, input_channels, stride, out_channels=None):
        super().__init__()
        if stride == 1:
            branch_channel = int(input_channels / 2)
        else:
            branch_channel = input_channels

        if out_channels is None:
            self.in_channels = int(branch_channel)
        else:
            self.in_channels = int(out_channels / 2)

        self._depthwise_separable_0 = _SeparableConvBNReLU(
            input_channels, self.in_channels, 3, stride=stride)
        self._conv = _ConvBNReLU(
            branch_channel, self.in_channels, 1, stride=1, padding=0)
        self._depthwise_separable_1 = _SeparableConvBNReLU(
            self.in_channels, self.in_channels, 3, stride=stride)

        self.stride = stride

    def forward(self, input):

        if self.stride == 1:
            shortcut, branch = paddle.split(x=input, num_or_sections=2, axis=1)
        else:
            branch = input
            shortcut = self._depthwise_separable_0(input)

        branch_1x1 = self._conv(branch)
        branch_dw1x1 = self._depthwise_separable_1(branch_1x1)
        output = paddle.concat(x=[shortcut, branch_dw1x1], axis=1)

        # channel shuffle
        out_shape = paddle.shape(output)
        h, w = out_shape[2], out_shape[3]
        output = paddle.reshape(x=output, shape=[0, 2, self.in_channels, h, w])
        output = paddle.transpose(x=output, perm=[0, 2, 1, 3, 4])
        output = paddle.reshape(
            x=output, shape=[0, 2 * self.in_channels, h, w])
        return output


if __name__ == '__main__':
    import numpy as np
    import os

    np.random.seed(100)
    paddle.seed(100)

    net = ShuffleNetV2(10)
    img = np.random.random(size=(4, 3, 100, 100)).astype('float32')
    img = paddle.to_tensor(img)
    out = net(img)
    print(out)

    net.forward = paddle.jit.to_static(net.forward)
    save_path = os.path.join('.', 'model')
    in_var = paddle.ones([4, 3, 100, 100])
    paddle.jit.save(net, save_path, input_spec=[in_var])
