
import torch
from torch import nn
from audiossl.modules.transformer import Block
from torch.nn import functional as F
from functools import partial
import time
import warnings
import math

def _no_grad_trunc_normal_(tensor, mean, std, a, b):
    # Cut & paste from PyTorch official master until it's in a few official releases - RW
    # Method based on https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf
    def norm_cdf(x):
        # Computes standard normal cumulative distribution function
        return (1. + math.erf(x / math.sqrt(2.))) / 2.

    if (mean < a - 2 * std) or (mean > b + 2 * std):
        warnings.warn("mean is more than 2 std from [a, b] in nn.init.trunc_normal_. "
                      "The distribution of values may be incorrect.",
                      stacklevel=2)

    with torch.no_grad():
        # Values are generated by using a truncated uniform distribution and
        # then using the inverse CDF for the normal distribution.
        # Get upper and lower cdf values
        l = norm_cdf((a - mean) / std)
        u = norm_cdf((b - mean) / std)

        # Uniformly fill tensor with values from [l, u], then translate to
        # [2l-1, 2u-1].
        tensor.uniform_(2 * l - 1, 2 * u - 1)

        # Use inverse cdf transform for normal distribution to get truncated
        # standard normal
        tensor.erfinv_()

        # Transform to proper mean, std
        tensor.mul_(std * math.sqrt(2.))
        tensor.add_(mean)

        # Clamp to ensure it's in the proper range
        tensor.clamp_(min=a, max=b)
        return tensor


def trunc_normal_(tensor, mean=0., std=1., a=-2., b=2.):
    # type: (Tensor, float, float, float, float) -> Tensor
    return _no_grad_trunc_normal_(tensor, mean, std, a, b)


def get_num_patches(height=64,width=1001,patch_height=16,patch_width=16):
    return (height // patch_height) * (width // patch_width)

from einops.layers.torch import Rearrange
class PatchEmbed_v2(nn.Module):
    def __init__(self,patch_height=16,patch_width=16,embed_dim=768):
        super().__init__()
        self.patch_height = patch_height
        self.patch_width = patch_width
        self.patch_maker = Rearrange('b c (h p1) (w p2) -> b (w h) (p1 p2 c)', p1 = patch_height, p2 = patch_width)
        self.patch_embed = nn.Linear(patch_height*patch_width,embed_dim)
        
    def forward(self,melspec,length=None):
        height = melspec.shape[2] - melspec.shape[2]%self.patch_height
        width = melspec.shape[3] - melspec.shape[3]%self.patch_width
        patch = self.patch_maker(melspec[:,:,:height,:width])
        patch_embed = self.patch_embed(patch)

        if length is not None:
            patch_length = (height//self.patch_height) * ((length - length%self.patch_width)//self.patch_width)
        else:
            patch_length = None

        return patch,patch_embed,patch_length


class AST(nn.Module):
    """ Vision Transformer """
    def __init__(self,use_cls=True, spec_h=64,spec_w=1001, patch_w=16,patch_h=16, in_chans=1, num_classes=0, embed_dim=768, depth=12,
                 num_heads=12, mlp_ratio=4., qkv_bias=False, qk_scale=None, drop_rate=0., attn_drop_rate=0.,
                 drop_path_rate=0.1, norm_layer=nn.LayerNorm,mask_ratio=0,pos_type="cut", **kwargs):
        super().__init__()
        self.num_features = self.embed_dim = embed_dim
        self.spec_w = spec_w
        self.spec_h = spec_h
        self.embed_dim = embed_dim
        self.patch_w = patch_w
        self.patch_h = patch_h


        self.patch_embed = PatchEmbed_v2(patch_h,patch_w,embed_dim)
        self.mask_embed = nn.Parameter(torch.zeros(1,1, self.embed_dim))

        num_patches = get_num_patches(spec_h,spec_w,patch_h,patch_w)
        self.num_patches = num_patches
        self.use_cls = use_cls

        if use_cls:
            self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))

        self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim))
        self.pos_drop = nn.Dropout(p=drop_rate)
        self.mask_ratio = mask_ratio
        self.pos_type = pos_type

        dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]  # stochastic depth decay rule
        self.blocks = nn.ModuleList([
            Block(
                dim=embed_dim, num_heads=num_heads, mlp_ratio=mlp_ratio, qkv_bias=qkv_bias, qk_scale=qk_scale,
                drop=drop_rate, attn_drop=attn_drop_rate, drop_path=dpr[i], norm_layer=norm_layer)
            for i in range(depth)])
        self.norm = norm_layer(embed_dim)
        if self.mask_ratio > 0:
            self.final_to_melsp = nn.Linear(embed_dim,patch_h*patch_w)


        trunc_normal_(self.pos_embed, std=.02)
        trunc_normal_(self.mask_embed, std=.02)
        if use_cls:
            trunc_normal_(self.cls_token, std=.02)
        self.apply(self._init_weights)

    def _init_weights(self, m):
        if isinstance(m, nn.Linear):
            trunc_normal_(m.weight, std=.02)
            if isinstance(m, nn.Linear) and m.bias is not None:
                nn.init.constant_(m.bias, 0)
        elif isinstance(m, nn.LayerNorm):
            nn.init.constant_(m.bias, 0)
            nn.init.constant_(m.weight, 1.0)

    def interpolate_pos_encoding(self, x, h, w):
        npatch = x.shape[1] - 1
        N = self.pos_embed.shape[1] - 1
        if npatch == N and w == self.spec_w and h == self.spec_h:
            return self.pos_embed
        class_pos_embed = self.pos_embed[:, 0]
        patch_pos_embed = self.pos_embed[:, 1:]
        dim = x.shape[-1]
        w0 = w // self.patch_embed.patch_width
        h0 = h // self.patch_embed.patch_height
        # we add a small number to avoid floating point error in the interpolation
        # see discussion at https://github.com/facebookresearch/dino/issues/8
        w0, h0 = w0 + 0.1, h0 + 0.1
        patch_pos_embed = nn.functional.interpolate(
            patch_pos_embed.reshape(1, self.spec_h//self.patch_h, self.spec_w//self.patch_w, dim).permute(0, 3, 1, 2),
            scale_factor=(h0 / (self.spec_h//self.patch_h), w0 / (self.spec_w//self.patch_w)),
            mode='bicubic',
        )
        assert int(h0) == patch_pos_embed.shape[-2] and int(w0) == patch_pos_embed.shape[-1]
        patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(1, -1, dim)
        return torch.cat((class_pos_embed.unsqueeze(0), patch_pos_embed), dim=1)

    def prepare_tokens(self, x, mask_index, length, mask=True):
        B, nc, h, w = x.shape
        mel_patches,x,patch_length = self.patch_embed(x,length)  # patch linear embedding
        B, T, C = x.shape

        if (mask_index is not None) and mask:
            mask_index_expand = mask_index.unsqueeze(2).expand(B,T,self.embed_dim).float()
            x = (1-mask_index_expand) * x + mask_index_expand * self.mask_embed.expand(B,T,C)

        # add the [CLS] token to the embed patch tokens
        if self.use_cls:
            cls_tokens = self.cls_token.expand(B, -1, -1)
            x = torch.cat((cls_tokens, x), dim=1)


            # add positional encoding to each token

            if self.pos_type == "cut":
                pos = self.pos_embed[:,:T+1,:].expand(B,-1,-1) 
                x = x + pos
            else:
                if patch_length is not None:
                    assert all([patch_length[i] + 1 ==  x.shape[1] for i in range(B)]) or print(x.shape,patch_length)
                pos = self.interpolate_pos_encoding(x,h,w)
                x = x + pos
        else:
            if self.pos_type == "cut":
                pos = self.pos_embed[:,:T,:].expand(B,-1,-1) 
                x = x + pos
            else:
                print("don't support position type interplation when use_cls is set to be False")
                exit()

        return self.pos_drop(x),pos,mel_patches,h,w,patch_length

    def forward(self, x, mask_index=None,length=None,avg=False):
        x,pos,mel_patches,h,w,patch_length = self.prepare_tokens(x,mask_index,length)

        avg_x = []
        for i,blk in enumerate(self.blocks):
            if self.use_cls:
                x = blk(x,patch_length+1)
            else:
                x = blk(x,patch_length)
            if avg:
                if i> len(self.blocks)-9 :
                    avg_x.append(x)
                    #avg_x.append(F.instance_norm(x.transpose(1,2)).transpose(1,2))
        x = self.norm(x)
        if avg:
            avg_x=torch.mean(torch.stack(avg_x),dim=0)

        if self.mask_ratio>0:
            if patch_length is not None:
                length_mask = torch.arange(mel_patches.shape[1]).to(x.device) < patch_length.unsqueeze(1)
                length_mask = length_mask.to(x.device)
                mask = mask_index & length_mask
            else:
                mask = mask_index
                
            logits = self.final_to_melsp(x[:,1:][mask])
            mse_loss = F.mse_loss(logits,mel_patches[mask])
        else:
            mse_loss = torch.zeros([])
        
        if self.use_cls:
            if avg:
                return avg_x[:,0]
            else:
                return x[:,0]
        else:
            if patch_length is not None:
                length_mask = torch.arange(mel_patches.shape[1]).to(x.device) < patch_length.unsqueeze(1)
                length_mask = length_mask.to(x.device)

            if avg:
                avg_t = torch.sum(avg_x*length_mask.unsqueeze(2),dim=1)/patch_length.unsqueeze(1)
                return avg_t
            else:
                avg_t = torch.sum(x*length_mask.unsqueeze(2),dim=1)/patch_length.unsqueeze(1)
                return avg_t

    def get_last_selfattention(self, x):
        x,_,_,_,_,_ = self.prepare_tokens(x,mask_index=None,length=None,mask=False)
        for i, blk in enumerate(self.blocks):
            if i < len(self.blocks) - 1:
                x = blk(x)
            else:
                # return attention of the last block
                return blk(x, return_attention=True)
    def get_intermediate_layers(self, x,length, n=1):
        x,_,_,_,_,patch_length = self.prepare_tokens(x,mask_index=None,length=length,mask=False)
        # we return the output tokens from the `n` last blocks
        output = []
        for i, blk in enumerate(self.blocks):
            if patch_length is None:
                x = blk(x,length=None)
                if self.use_cls:
                    x_ = x
                else:
                    x_ = torch.cat([torch.zeros_like(x[:,0:1]),x],dim=1)
            else:
                if self.use_cls:
                    x = blk(x,length=patch_length+1)
                    x_ = x
                else:
                    x = blk(x,length=patch_length)
                    x_ = torch.cat([torch.zeros_like(x[:,0:1]),x],dim=1)
            if len(self.blocks) - i <= n:
                output.append(self.norm(x_))
        return output
        
    def get_intermediate_layers_chunks(self, x,length, n=1,  chunk_len=401, avgpool=True):
        total_len = x.shape[-1]
        num_chunks = total_len // chunk_len + 1
        cls = []
        avg = []
        chunk_mark = []
        for i in range(num_chunks):
            cur_len = torch.clip(length - i*chunk_len,0)
            if i==0:
                chunk_mark_ = cur_len > 0
            else:
                chunk_mark_ = cur_len > chunk_len//2
            start = i*chunk_len
            end = (i+1) * chunk_len
            if end > total_len:
                end = total_len
            x_chunk=x[:,:,:,start:end]
            x_chunk,_,_,_,_,patch_length = self.prepare_tokens(x_chunk,mask_index=None,length=cur_len,mask=False)
            # we return the output tokens from the `n` last blocks
            output_i = []
            for j, blk in enumerate(self.blocks):
                if patch_length is None:
                    x_chunk = blk(x_chunk,length=None)
                else:
                    if self.use_cls:
                        x_chunk= blk(x_chunk,length=patch_length+1)
                    else:
                        x_chunk= blk(x_chunk,length=patch_length)
                if len(self.blocks) - j <= n:
                    output_i.append(self.norm(x_chunk))
            cls_,avg_=get_cls_avg(output_i,patch_length,self.use_cls)
            """
            print("{}-th chunk".format(i))
            print(len(cls_),cls_[0].shape)
            print(len(avg_),avg_[0].shape)
            print(length)
            print(cur_len)
            print(chunk_mark_.shape)
            print("{}-th chunk".format(i))
            """
            cls.append(cls_)
            avg.append(avg_)
            chunk_mark.append([chunk_mark_]*n)
        cls = [list(x) for x in zip(*cls)]
        avg = [list(x) for x in zip(*avg)]
        chunk_mark = [list(x) for x in zip(*chunk_mark)]
        """
        print(len(cls))
        print(type(cls[0]))
        print(len(cls[0]))
        """

        cls_out = []
        avg_out = []
        for cls_,avg_,chunk_mark_ in zip(cls,avg,chunk_mark):
            """
            print(type(cls_))
            print(type(avg_))
            print(type(chunk_mark_))
            print(len(cls_))
            print(len(avg_))
            print(len(chunk_mark_))
            print(type(cls_[0]))
            print(type(avg_[0]))
            print(type(chunk_mark_[0]))
            print(cls_[0].shape)
            print(avg_[0].shape)
            print(chunk_mark_[0].shape)
            """
            cls_ = torch.stack(cls_,dim=0)
            avg_ = torch.stack(avg_,dim=0)
            chunk_mark_ = torch.stack(chunk_mark_,dim=0)
            """
            print(cls_.shape)
            print(avg_.shape)
            print(cls_[0][0])
            print(cls_[1][0])
            print(avg_[0][0])
            print(avg_[1][0])
            print(chunk_mark_.shape)
            """
            cls_=torch.sum(cls_*chunk_mark_.unsqueeze(-1),dim=0)/torch.sum(chunk_mark_.unsqueeze(-1),dim=0)
            avg_=torch.sum(avg_*chunk_mark_.unsqueeze(-1),dim=0)/torch.sum(chunk_mark_.unsqueeze(-1),dim=0)
            """
            print(cls_[0])
            print(avg_[0])
            print(cls_.shape)
            print(avg_.shape)
            """

            cls_out.append(cls_)
            avg_out.append(avg_)
        if avgpool:
            return torch.cat(cls_out+avg_out,dim=-1)
        else:
            return torch.cat(cls_out,dim=-1)
        #return cls_out,avg_out

def get_cls_avg(output_i,cur_len,use_cls):
    if use_cls:
        length_mask = torch.arange(output_i[0].shape[1]-1).to(output_i[0].device) < cur_len.unsqueeze(1)
    else:
        length_mask = torch.arange(output_i[0].shape[1]).to(output_i[0].device) < cur_len.unsqueeze(1)
    if use_cls:
        cls = [x[:,0] for x in output_i]
        avg = [torch.sum(x[:,1:]*length_mask.unsqueeze(-1),dim=1)/(cur_len.unsqueeze(1)+1e-6) for x in output_i]
    else:
        cls = [torch.zeros_like(x[:,0]) for x in output_i]
        avg = [torch.sum(x*length_mask.unsqueeze(-1),dim=1)/(cur_len.unsqueeze(1)+1e-6) for x in output_i]
    return cls,avg
def AST_small(patch_h=64,patch_w=4,**kwargs):
    return AST(patch_h=patch_h,patch_w=patch_w,embed_dim=384,depth=12,num_heads=6,qkv_bias=False,norm_layer=partial(nn.LayerNorm, eps=1e-6),**kwargs)

def AST_base(patch_h=64,patch_w=4,**kwargs):
    return AST(patch_h=patch_h,patch_w=patch_w,embed_dim=768,depth=12,num_heads=12,qkv_bias=False,norm_layer=partial(nn.LayerNorm, eps=1e-6),**kwargs)

def AST_large(patch_h,patch_w,**kwargs):
    return AST(patch_h=patch_h,patch_w=patch_w,embed_dim=1024,depth=24,num_heads=16,qkv_bias=False,norm_layer=partial(nn.LayerNorm, eps=1e-6),**kwargs)
