# Densenet decoder encoder with intermediate fully connected layers and dropout

import numpy as np
import torch
import torch.nn as nn


def add_coordConv_channels(t):
    n, c, h, w = t.size()
    xx_channel = np.ones((h, w))
    xx_range = np.array(range(h))
    xx_range = np.expand_dims(xx_range, -1)
    xx_coord = xx_channel * xx_range
    yy_coord = xx_coord.transpose()

    xx_coord = xx_coord / (h - 1)
    yy_coord = yy_coord / (h - 1)
    xx_coord = xx_coord * 2 - 1
    yy_coord = yy_coord * 2 - 1
    xx_coord = torch.from_numpy(xx_coord).float()
    yy_coord = torch.from_numpy(yy_coord).float()

    if t.is_cuda:
        xx_coord = xx_coord.cuda()
        yy_coord = yy_coord.cuda()

    xx_coord = xx_coord.unsqueeze(0).unsqueeze(0).repeat(n, 1, 1, 1)
    yy_coord = yy_coord.unsqueeze(0).unsqueeze(0).repeat(n, 1, 1, 1)

    t_cc = torch.cat((t, xx_coord, yy_coord), dim=1)

    return t_cc


class DenseBlockEncoder(nn.Module):
    def __init__(self, n_channels, n_convs, activation=nn.ReLU, args=[False]):
        super(DenseBlockEncoder, self).__init__()
        assert (n_convs > 0)

        self.n_channels = n_channels
        self.n_convs = n_convs
        self.layers = nn.ModuleList()
        for i in range(n_convs):
            self.layers.append(nn.Sequential(
                nn.BatchNorm2d(n_channels),
                activation(*args),
                nn.Conv2d(n_channels, n_channels, 3, stride=1, padding=1, bias=False), ))

    def forward(self, inputs):
        outputs = []

        for i, layer in enumerate(self.layers):
            if i > 0:
                next_output = 0
                for no in outputs:
                    next_output = next_output + no
                outputs.append(next_output)
            else:
                outputs.append(layer(inputs))
        return outputs[-1]


# Dense block in encoder.
class DenseBlockDecoder(nn.Module):
    def __init__(self, n_channels, n_convs, activation=nn.ReLU, args=[False]):
        super(DenseBlockDecoder, self).__init__()
        assert (n_convs > 0)

        self.n_channels = n_channels
        self.n_convs = n_convs
        self.layers = nn.ModuleList()
        for i in range(n_convs):
            self.layers.append(nn.Sequential(
                nn.BatchNorm2d(n_channels),
                activation(*args),
                nn.ConvTranspose2d(n_channels, n_channels, 3, stride=1, padding=1, bias=False), ))

    def forward(self, inputs):
        outputs = []

        for i, layer in enumerate(self.layers):
            if i > 0:
                next_output = 0
                for no in outputs:
                    next_output = next_output + no
                outputs.append(next_output)
            else:
                outputs.append(layer(inputs))
        return outputs[-1]


class DenseTransitionBlockEncoder(nn.Module):
    def __init__(self, n_channels_in, n_channels_out, mp, activation=nn.ReLU, args=[False]):
        super(DenseTransitionBlockEncoder, self).__init__()
        self.n_channels_in = n_channels_in
        self.n_channels_out = n_channels_out
        self.mp = mp
        self.main = nn.Sequential(
            nn.BatchNorm2d(n_channels_in),
            activation(*args),
            nn.Conv2d(n_channels_in, n_channels_out, 1, stride=1, padding=0, bias=False),
            nn.MaxPool2d(mp),
        )

    def forward(self, inputs):
        return self.main(inputs)


class DenseTransitionBlockDecoder(nn.Module):
    def __init__(self, n_channels_in, n_channels_out, activation=nn.ReLU, args=[False]):
        super(DenseTransitionBlockDecoder, self).__init__()
        self.n_channels_in = n_channels_in
        self.n_channels_out = n_channels_out
        self.main = nn.Sequential(
            nn.BatchNorm2d(n_channels_in),
            activation(*args),
            nn.ConvTranspose2d(n_channels_in, n_channels_out, 4, stride=2, padding=1, bias=False),
        )

    def forward(self, inputs):
        return self.main(inputs)


## Dense encoders and decoders for image of size 128 128
class waspDenseEncoder128(nn.Module):
    def __init__(self, nc=1, ndf=32, ndim=128, activation=nn.LeakyReLU, args=[0.2, False], f_activation=nn.Tanh,
                 f_args=[]):
        super(waspDenseEncoder128, self).__init__()
        self.ndim = ndim

        self.main = nn.Sequential(
            # input is (nc) x 128 x 128
            nn.BatchNorm2d(nc),
            nn.ReLU(True),
            nn.Conv2d(nc, ndf, 4, stride=2, padding=1),

            # state size. (ndf) x 64 x 64
            DenseBlockEncoder(ndf, 6),
            DenseTransitionBlockEncoder(ndf, ndf * 2, 2, activation=activation, args=args),

            # state size. (ndf*2) x 32 x 32
            DenseBlockEncoder(ndf * 2, 12),
            DenseTransitionBlockEncoder(ndf * 2, ndf * 4, 2, activation=activation, args=args),

            # state size. (ndf*4) x 16 x 16
            DenseBlockEncoder(ndf * 4, 16),
            DenseTransitionBlockEncoder(ndf * 4, ndf * 8, 2, activation=activation, args=args),

            # state size. (ndf*4) x 8 x 8
            DenseBlockEncoder(ndf * 8, 16),
            DenseTransitionBlockEncoder(ndf * 8, ndf * 8, 2, activation=activation, args=args),

            # state size. (ndf*8) x 4 x 4
            DenseBlockEncoder(ndf * 8, 16),
            DenseTransitionBlockEncoder(ndf * 8, ndim, 4, activation=activation, args=args),
            f_activation(*f_args),
        )

    def forward(self, input):
        input = add_coordConv_channels(input)
        output = self.main(input).view(-1, self.ndim)
        # print(output.size())
        return output


class waspDenseDecoder128(nn.Module):
    def __init__(self, nz=128, nc=1, ngf=32, lb=0, ub=1, activation=nn.ReLU, args=[False], f_activation=nn.Hardtanh,
                 f_args=[]):
        super(waspDenseDecoder128, self).__init__()
        self.main = nn.Sequential(
            # input is Z, going into convolution
            nn.BatchNorm2d(nz),
            activation(*args),
            nn.ConvTranspose2d(nz, ngf * 8, 4, 1, 0, bias=False),

            # state size. (ngf*8) x 4 x 4
            DenseBlockDecoder(ngf * 8, 16),
            DenseTransitionBlockDecoder(ngf * 8, ngf * 8),

            # state size. (ngf*4) x 8 x 8
            DenseBlockDecoder(ngf * 8, 16),
            DenseTransitionBlockDecoder(ngf * 8, ngf * 4),

            # state size. (ngf*2) x 16 x 16
            DenseBlockDecoder(ngf * 4, 12),
            DenseTransitionBlockDecoder(ngf * 4, ngf * 2),

            # state size. (ngf) x 32 x 32
            DenseBlockDecoder(ngf * 2, 6),
            DenseTransitionBlockDecoder(ngf * 2, ngf),

            # state size. (ngf) x 64 x 64
            DenseBlockDecoder(ngf, 6),
            DenseTransitionBlockDecoder(ngf, ngf),

            # state size (ngf) x 128 x 128
            nn.BatchNorm2d(ngf),
            activation(*args),
            nn.ConvTranspose2d(ngf, nc, 3, stride=1, padding=1, bias=False),
            f_activation(*f_args),
        )
        # self.smooth=nn.Sequential(
        #     nn.Conv2d(nc, nc, 1, stride=1, padding=0, bias=False),
        #     f_activation(*f_args),
        # )

    def forward(self, inputs):
        # return self.smooth(self.main(inputs))
        return self.main(inputs)


class dnetccnl(nn.Module):
    # in_channels -> nc      | encoder first layer
    # filters -> ndf    | encoder first layer
    # img_size(h,w) -> ndim
    # out_channels  -> optical flow (x,y)

    def __init__(self, img_size=128, in_channels=1, out_channels=2, filters=32, fc_units=100):
        super(dnetccnl, self).__init__()
        self.nc = in_channels
        self.nf = filters
        self.ndim = img_size
        self.oc = out_channels
        self.fcu = fc_units

        self.encoder = waspDenseEncoder128(nc=self.nc + 2, ndf=self.nf, ndim=self.ndim)
        self.decoder = waspDenseDecoder128(nz=self.ndim, nc=self.oc, ngf=self.nf)
        # self.fc_layers= nn.Sequential(nn.Linear(self.ndim, self.fcu),
        #                               nn.ReLU(True),
        #                               nn.Dropout(0.25),
        #                               nn.Linear(self.fcu,self.ndim),
        #                               nn.ReLU(True),
        #                               nn.Dropout(0.25),
        #                               )

    def forward(self, inputs):
        encoded = self.encoder(inputs)
        encoded = encoded.unsqueeze(-1).unsqueeze(-1)
        decoded = self.decoder(encoded)
        # print torch.max(decoded)
        # print torch.min(decoded)

        return decoded
