#copyright (c) 2019 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

from .base_network import conv2d, deconv2d, norm_layer
import paddle.fluid as fluid


class Pix2pix_model(object):
    def __init__(self):
        pass

    def network_G(self, input, name, cfg):
        if cfg.net_G == 'resnet_9block':
            net = build_generator_resnet_blocks(
                input,
                name=name + "_resnet9block",
                n_gen_res=9,
                g_base_dims=cfg.g_base_dims,
                use_dropout=cfg.dropout,
                norm_type=cfg.norm_type)
        elif cfg.net_G == 'resnet_6block':
            net = build_generator_resnet_blocks(
                input,
                name=name + "_resnet6block",
                n_gen_res=6,
                g_base_dims=cfg.g_base_dims,
                use_dropout=cfg.dropout,
                norm_type=cfg.norm_type)
        elif cfg.net_G == 'unet_128':
            net = build_generator_Unet(
                input,
                name=name + "_unet128",
                num_downsample=7,
                g_base_dims=cfg.g_base_dims,
                use_dropout=cfg.dropout,
                norm_type=cfg.norm_type)
        elif cfg.net_G == 'unet_256':
            net = build_generator_Unet(
                input,
                name=name + "_unet256",
                num_downsample=8,
                g_base_dims=cfg.g_base_dims,
                use_dropout=cfg.dropout,
                norm_type=cfg.norm_type)
        else:
            raise NotImplementedError(
                'network G: [%s] is wrong format, please check it' % cfg.net_G)
        return net

    def network_D(self, input, name, cfg):
        if cfg.net_D == 'basic':
            net = build_discriminator_Nlayers(
                input,
                name=name + '_basic',
                d_nlayers=3,
                d_base_dims=cfg.d_base_dims,
                norm_type=cfg.norm_type)
        elif cfg.net_D == 'nlayers':
            net = build_discriminator_Nlayers(
                input,
                name=name + '_nlayers',
                d_nlayers=cfg.d_nlayers,
                d_base_dims=cfg.d_base_dims,
                norm_type=cfg.norm_type)
        elif cfg.net_D == 'pixel':
            net = build_discriminator_Pixel(
                input,
                name=name + '_pixel',
                d_base_dims=cfg.d_base_dims,
                norm_type=cfg.norm_type)
        else:
            raise NotImplementedError(
                'network D: [%s] is wrong format, please check it' % cfg.net_D)
        return net


def build_resnet_block(inputres,
                       dim,
                       name="resnet",
                       use_bias=False,
                       use_dropout=False,
                       norm_type='batch_norm'):
    out_res = fluid.layers.pad2d(inputres, [1, 1, 1, 1], mode="reflect")
    out_res = conv2d(
        input=out_res,
        num_filters=dim,
        filter_size=3,
        stride=1,
        stddev=0.02,
        name=name + "_c1",
        norm=norm_type,
        activation_fn='relu',
        use_bias=use_bias)

    if use_dropout:
        out_res = fluid.layers.dropout(out_res, dropout_prob=0.5)

    out_res = fluid.layers.pad2d(out_res, [1, 1, 1, 1], mode="reflect")
    out_res = conv2d(
        input=out_res,
        num_filters=dim,
        filter_size=3,
        stride=1,
        stddev=0.02,
        name=name + "_c2",
        norm=norm_type,
        use_bias=use_bias)
    return out_res + inputres


def build_generator_resnet_blocks(inputgen,
                                  name="generator",
                                  n_gen_res=9,
                                  g_base_dims=64,
                                  use_dropout=False,
                                  norm_type='batch_norm'):
    ''' generator use resnet block'''
    '''The shape of input should be equal to the shape of output.'''
    use_bias = norm_type == 'instance_norm'
    pad_input = fluid.layers.pad2d(inputgen, [3, 3, 3, 3], mode="reflect")
    o_c1 = conv2d(
        input=pad_input,
        num_filters=g_base_dims,
        filter_size=7,
        stride=1,
        stddev=0.02,
        name=name + "_c1",
        norm=norm_type,
        activation_fn='relu')
    o_c2 = conv2d(
        input=o_c1,
        num_filters=g_base_dims * 2,
        filter_size=3,
        stride=2,
        stddev=0.02,
        padding=1,
        name=name + "_c2",
        norm=norm_type,
        activation_fn='relu')
    res_input = conv2d(
        input=o_c2,
        num_filters=g_base_dims * 4,
        filter_size=3,
        stride=2,
        stddev=0.02,
        padding=1,
        name=name + "_c3",
        norm=norm_type,
        activation_fn='relu')
    for i in range(n_gen_res):
        conv_name = name + "_r{}".format(i + 1)
        res_output = build_resnet_block(
            res_input,
            g_base_dims * 4,
            name=conv_name,
            use_bias=use_bias,
            use_dropout=use_dropout)
        res_input = res_output

    o_c4 = deconv2d(
        input=res_output,
        num_filters=g_base_dims * 2,
        filter_size=3,
        stride=2,
        stddev=0.02,
        padding=[1, 1],
        outpadding=[0, 1, 0, 1],
        name=name + "_c4",
        norm=norm_type,
        activation_fn='relu')
    o_c5 = deconv2d(
        input=o_c4,
        num_filters=g_base_dims,
        filter_size=3,
        stride=2,
        stddev=0.02,
        padding=[1, 1],
        outpadding=[0, 1, 0, 1],
        name=name + "_c5",
        norm=norm_type,
        activation_fn='relu')
    o_p2 = fluid.layers.pad2d(o_c5, [3, 3, 3, 3], mode="reflect")
    o_c6 = conv2d(
        input=o_p2,
        num_filters=3,
        filter_size=7,
        stride=1,
        stddev=0.02,
        name=name + "_c6",
        activation_fn='tanh',
        use_bias=True)

    return o_c6


def Unet_block(inputunet,
               i,
               outer_dim,
               inner_dim,
               num_downsample,
               innermost=False,
               outermost=False,
               norm_type='batch_norm',
               use_bias=False,
               use_dropout=False,
               name=None):
    if outermost == True:
        downconv = conv2d(
            input=inputunet,
            num_filters=inner_dim,
            filter_size=4,
            stride=2,
            stddev=0.02,
            padding=1,
            name=name + '_outermost_dc1',
            use_bias=True)
        i += 1
        mid_block = Unet_block(
            downconv,
            i,
            inner_dim,
            inner_dim * 2,
            num_downsample,
            norm_type=norm_type,
            use_bias=use_bias,
            use_dropout=use_dropout,
            name=name)
        uprelu = fluid.layers.relu(mid_block, name=name + '_outermost_relu')
        updeconv = deconv2d(
            input=uprelu,
            num_filters=outer_dim,
            filter_size=4,
            stride=2,
            stddev=0.02,
            padding=1,
            name=name + '_outermost_uc1',
            activation_fn='tanh',
            use_bias=use_bias)
        return updeconv
    elif innermost == True:
        downrelu = fluid.layers.leaky_relu(
            inputunet, 0.2, name=name + '_innermost_leaky_relu')
        upconv = conv2d(
            input=downrelu,
            num_filters=inner_dim,
            filter_size=4,
            stride=2,
            stddev=0.02,
            padding=1,
            name=name + '_innermost_dc1',
            activation_fn='relu',
            use_bias=use_bias)
        updeconv = deconv2d(
            input=upconv,
            num_filters=outer_dim,
            filter_size=4,
            stride=2,
            stddev=0.02,
            padding=1,
            name=name + '_innermost_uc1',
            norm=norm_type,
            use_bias=use_bias)
        return fluid.layers.concat([inputunet, updeconv], 1)
    else:
        downrelu = fluid.layers.leaky_relu(
            inputunet, 0.2, name=name + '_leaky_relu')
        downnorm = conv2d(
            input=downrelu,
            num_filters=inner_dim,
            filter_size=4,
            stride=2,
            stddev=0.02,
            padding=1,
            name=name + 'dc1',
            norm=norm_type,
            use_bias=use_bias)
        i += 1
        if i < 4:
            mid_block = Unet_block(
                downnorm,
                i,
                inner_dim,
                inner_dim * 2,
                num_downsample,
                norm_type=norm_type,
                use_bias=use_bias,
                name=name + '_mid{}'.format(i))
        elif i < num_downsample - 1:
            mid_block = Unet_block(
                downnorm,
                i,
                inner_dim,
                inner_dim,
                num_downsample,
                norm_type=norm_type,
                use_bias=use_bias,
                use_dropout=use_dropout,
                name=name + '_mid{}'.format(i))
        else:
            mid_block = Unet_block(
                downnorm,
                i,
                inner_dim,
                inner_dim,
                num_downsample,
                innermost=True,
                norm_type=norm_type,
                use_bias=use_bias,
                name=name + '_innermost')
        uprelu = fluid.layers.relu(mid_block, name=name + '_relu')
        updeconv = deconv2d(
            input=uprelu,
            num_filters=outer_dim,
            filter_size=4,
            stride=2,
            stddev=0.02,
            padding=1,
            name=name + '_uc1',
            norm=norm_type,
            use_bias=use_bias)

        if use_dropout:
            upnorm = fluid.layers.dropout(upnorm, dropout_prob=0.5)
        return fluid.layers.concat([inputunet, updeconv], 1)


def UnetSkipConnectionBlock(input,
                            i,
                            num_downs,
                            outer_nc,
                            inner_nc,
                            outermost=False,
                            innermost=False,
                            norm='batch_norm',
                            use_dropout=False,
                            name=""):
    use_bias = norm == "instance"
    if outermost:
        downconv = conv2d(
            input,
            num_filters=inner_nc,
            filter_size=4,
            stride=2,
            padding=1,
            use_bias=use_bias,
            name=name + '_down_conv')
        i += 1
        ngf = inner_nc
        sub_res = UnetSkipConnectionBlock(
            downconv,
            i,
            num_downs,
            outer_nc=ngf,
            inner_nc=ngf * 2,
            norm=norm,
            name=name + '_u%d' % i)
        uprelu = fluid.layers.relu(sub_res)
        upconv = deconv2d(
            input=uprelu,
            num_filters=outer_nc,
            filter_size=4,
            stride=2,
            padding=1,
            activation_fn='tanh',
            name=name + '_up_conv')
        return upconv
    elif innermost:
        downrelu = fluid.layers.leaky_relu(input, 0.2)
        downconv = conv2d(
            input=downrelu,
            num_filters=inner_nc,
            filter_size=4,
            stride=2,
            padding=1,
            use_bias=use_bias,
            name=name + '_down_conv')
        uprelu = fluid.layers.relu(downconv)
        upconv = deconv2d(
            input=uprelu,
            num_filters=outer_nc,
            filter_size=4,
            stride=2,
            padding=1,
            use_bias=use_bias,
            norm=norm,
            name=name + '_up_conv')
        return fluid.layers.concat([input, upconv], 1)
    else:
        downrelu = fluid.layers.leaky_relu(input, 0.2)
        downconv = conv2d(
            input=downrelu,
            num_filters=inner_nc,
            filter_size=4,
            stride=2,
            padding=1,
            use_bias=use_bias,
            norm=norm,
            name=name + '_down_conv')
        i += 1
        ngf = inner_nc
        if i < 4:
            sub_res = UnetSkipConnectionBlock(
                downconv,
                i,
                num_downs,
                outer_nc=ngf,
                inner_nc=ngf * 2,
                norm=norm,
                name=name + '_u%d' % i)
        elif i < num_downs - 1:
            sub_res = UnetSkipConnectionBlock(
                downconv,
                i,
                num_downs,
                outer_nc=ngf,
                inner_nc=ngf,
                norm=norm,
                name=name + '_u%d' % i)

        else:
            sub_res = UnetSkipConnectionBlock(
                downconv,
                i,
                num_downs,
                outer_nc=ngf,
                inner_nc=ngf,
                innermost=True,
                norm=norm,
                name=name + '_u%d' % i)

        uprelu = fluid.layers.relu(sub_res)
        upconv = deconv2d(
            input=uprelu,
            num_filters=outer_nc,
            filter_size=4,
            stride=2,
            padding=1,
            use_bias=use_bias,
            norm=norm,
            name=name + '_up_conv')
        out = upconv
        if use_dropout:
            out = fluid.layers.dropout(out, 0.5)
        return fluid.layers.concat([input, out], 1)


def build_generator_Unet(input,
                         name="",
                         num_downsample=8,
                         g_base_dims=64,
                         use_dropout=False,
                         norm_type='batch_norm'):
    ''' generator use Unet'''
    i = 0
    output = UnetSkipConnectionBlock(
        input,
        i,
        num_downsample,
        3,
        g_base_dims,
        outermost=True,
        norm=norm_type,
        name=name + '_u%d' % i)

    return output


def build_discriminator_Nlayers(inputdisc,
                                name="discriminator",
                                d_nlayers=3,
                                d_base_dims=64,
                                norm_type='batch_norm'):
    use_bias = norm_type != 'batch_norm'
    dis_input = conv2d(
        input=inputdisc,
        num_filters=d_base_dims,
        filter_size=4,
        stride=2,
        stddev=0.02,
        padding=1,
        name=name + "_c1",
        activation_fn='leaky_relu',
        relufactor=0.2,
        use_bias=True)
    d_dims = d_base_dims
    for i in range(d_nlayers - 1):
        conv_name = name + "_c{}".format(i + 2)
        d_dims *= 2
        dis_output = conv2d(
            input=dis_input,
            num_filters=d_dims,
            filter_size=4,
            stride=2,
            stddev=0.02,
            padding=1,
            name=conv_name,
            norm=norm_type,
            activation_fn='leaky_relu',
            relufactor=0.2,
            use_bias=use_bias)
        dis_input = dis_output
    last_dims = min(2**d_nlayers, 8)
    o_c4 = conv2d(
        input=dis_output,
        num_filters=d_base_dims * last_dims,
        filter_size=4,
        stride=1,
        stddev=0.02,
        padding=1,
        name=name + "_c{}".format(d_nlayers + 1),
        norm=norm_type,
        activation_fn='leaky_relu',
        relufactor=0.2,
        use_bias=use_bias)
    o_c5 = conv2d(
        input=o_c4,
        num_filters=1,
        filter_size=4,
        stride=1,
        stddev=0.02,
        padding=1,
        name=name + "_c{}".format(d_nlayers + 2),
        use_bias=True)
    return o_c5


def build_discriminator_Pixel(inputdisc,
                              name="discriminator",
                              d_base_dims=64,
                              norm_type='batch_norm'):
    use_bias = norm_type != 'instance_norm'
    o_c1 = conv2d(
        input=inputdisc,
        num_filters=d_base_dims,
        filter_size=1,
        stride=1,
        stddev=0.02,
        name=name + '_c1',
        activation_fn='leaky_relu',
        relufactor=0.2,
        use_bias=True)
    o_c2 = conv2d(
        input=o_c1,
        num_filters=d_base_dims * 2,
        filter_size=1,
        stride=1,
        stddev=0.02,
        name=name + '_c2',
        norm=norm_type,
        activation_fn='leaky_relu',
        relufactor=0.2,
        use_bias=use_bias)
    o_c3 = conv2d(
        o_c2,
        num_filters=1,
        filter_size=1,
        stride=1,
        stddev=0.02,
        name=name + '_c3',
        use_bias=use_bias)
    return o_c3
