import paddle
import paddle.nn as nn
import numpy as np
from ppgan.utils.download import get_path_from_url
model_urls = {
    'caffevgg19': ('https://paddlegan.bj.bcebos.com/models/vgg19_no_fc.npy',
                   '8ea1ef2374f8684b6cea9f300849be81')
}


class CaffeVGG19(nn.Layer):
    cfg = [
        64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512,
        'M', 512, 512, 512, 512, 'M'
    ]

    def __init__(self, output_index: int = 26) -> None:
        super().__init__()
        arch = 'caffevgg19'
        weights_path = get_path_from_url(model_urls[arch][0],
                                         model_urls[arch][1])
        data_dict: dict = np.load(weights_path,
                                  encoding='latin1',
                                  allow_pickle=True).item()
        self.features = self.make_layers(self.cfg, data_dict)
        del data_dict
        self.features = nn.Sequential(*self.features.sublayers()[:output_index])
        mean = paddle.to_tensor([103.939, 116.779, 123.68])
        self.mean = mean.unsqueeze(0).unsqueeze(-1).unsqueeze(-1)

    def _process(self, x):
        rgb = (x * 0.5 + 0.5) * 255  # value to 255
        bgr = paddle.stack((rgb[:, 2, :, :], rgb[:, 1, :, :], rgb[:, 0, :, :]),
                           1)  # rgb to bgr
        return bgr - self.mean  # vgg norm

    def _forward_impl(self, x):
        x = self._process(x)
        # NOTE get output with out relu activation
        x = self.features(x)
        return x

    def forward(self, x):
        return self._forward_impl(x)

    @staticmethod
    def get_conv_filter(data_dict, name):
        return data_dict[name][0]

    @staticmethod
    def get_bias(data_dict, name):
        return data_dict[name][1]

    @staticmethod
    def get_fc_weight(data_dict, name):
        return data_dict[name][0]

    def make_layers(self, cfg, data_dict, batch_norm=False) -> nn.Sequential:
        layers = []
        in_channels = 3
        block = 1
        number = 1
        for v in cfg:
            if v == 'M':
                layers += [nn.MaxPool2D(kernel_size=2, stride=2)]
                block += 1
                number = 1
            else:
                conv2d = nn.Conv2D(in_channels, v, kernel_size=3, padding=1)
                """ set value """
                weight = paddle.to_tensor(
                    self.get_conv_filter(data_dict, f'conv{block}_{number}'))
                weight = weight.transpose((3, 2, 0, 1))
                bias = paddle.to_tensor(
                    self.get_bias(data_dict, f'conv{block}_{number}'))
                conv2d.weight.set_value(weight)
                conv2d.bias.set_value(bias)
                number += 1
                if batch_norm:
                    layers += [conv2d, nn.BatchNorm2D(v), nn.ReLU()]
                else:
                    layers += [conv2d, nn.ReLU()]
                in_channels = v

        return nn.Sequential(*layers)
