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


import torch
from torch import nn

from einops import rearrange, repeat
from einops.layers.torch import Rearrange

#############

class PreNorm(nn.Module):
    def __init__(self, dim, fn):
        super().__init__()
        self.norm = nn.LayerNorm(dim)
        self.fn = fn
    def forward(self, x, **kwargs):
        return self.fn(self.norm(x), **kwargs)

class FeedForward(nn.Module):
    def __init__(self, dim, hidden_dim, dropout = 0.):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(dim, hidden_dim),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(hidden_dim, dim),
            nn.Dropout(dropout)
        )
    def forward(self, x):
        return self.net(x)

class Attention(nn.Module):
    def __init__(self, dim, heads = 8, dim_head = 64, dropout = 0.):
        super().__init__()
        inner_dim = dim_head *  heads
        project_out = not (heads == 1 and dim_head == dim)

        self.heads = heads
        self.scale = dim_head ** -0.5

        self.attend = nn.Softmax(dim = -1)
        self.dropout = nn.Dropout(dropout)

        self.to_qkv = nn.Linear(dim, inner_dim * 3, bias = False)

        self.to_out = nn.Sequential(
            nn.Linear(inner_dim, dim),
            nn.Dropout(dropout)
        ) if project_out else nn.Identity()

    def forward(self, x):
        qkv = self.to_qkv(x).chunk(3, dim = -1)
        q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = self.heads), qkv)

        dots = torch.matmul(q, k.transpose(-1, -2)) * self.scale

        attn = self.attend(dots)
        attn = self.dropout(attn)

        out = torch.matmul(attn, v)
        out = rearrange(out, 'b h n d -> b n (h d)')
        return self.to_out(out)

class Transformer(nn.Module):
    def __init__(self, dim, depth, heads, dim_head, mlp_dim, dropout = 0.):
        super().__init__()
        self.layers = nn.ModuleList([])
        for _ in range(depth):
            self.layers.append(nn.ModuleList([
                PreNorm(dim, Attention(dim, heads = heads, dim_head = dim_head, dropout = dropout)),
                PreNorm(dim, FeedForward(dim, mlp_dim, dropout = dropout))
            ]))
    def forward(self, x):
        for attn, ff in self.layers:
            x = attn(x) + x
            x = ff(x) + x
        return x
#############3




class NullPrompter(nn.Module):
    def __init__(self):
        super(NullPrompter, self).__init__()
        pass

    def forward(self, x):
        return x


class PadPrompter(nn.Module):
    def __init__(self, cfg):
        super(PadPrompter, self).__init__()
        pad_size = cfg.TRAINER.ZSROBUST.PROMPT_SIZE
        image_size = cfg.INPUT.SIZE
        image_size=image_size[0]

        self.base_size = image_size - pad_size*2
        self.pad_up = nn.Parameter(torch.randn([1, 3, pad_size, image_size]))
        self.pad_down = nn.Parameter(torch.randn([1, 3, pad_size, image_size]))
        self.pad_left = nn.Parameter(torch.randn([1, 3, image_size - pad_size*2, pad_size]))
        self.pad_right = nn.Parameter(torch.randn([1, 3, image_size - pad_size*2, pad_size]))

    def forward(self, x):
        base = torch.zeros(1, 3, self.base_size, self.base_size).cuda()
        prompt = torch.cat([self.pad_left, base, self.pad_right], dim=3)
        prompt = torch.cat([self.pad_up, prompt, self.pad_down], dim=2)
        prompt = torch.cat(x.size(0) * [prompt])

        return x + prompt

class TokenPrompter(nn.Module):
    def __init__(self, prompt_len) -> None:
        super(TokenPrompter, self).__init__()

        self.prompt = nn.Parameter(torch.randn([1, prompt_len, 768]))
    
    def forward(self):
        return self.prompt


class TokenPrompter_w_pos(nn.Module):
    def __init__(self, prompt_len) -> None:
        super(TokenPrompter_w_pos, self).__init__()

        self.prompt = nn.Parameter(torch.randn([1, prompt_len, 768]))
        self.pos_embedding = nn.Parameter(torch.randn(1, prompt_len, 1))

    def forward(self):
        return self.prompt + self.pos_embedding


class TokenPrompter_w_pos_TransformerGEN(nn.Module):
    def __init__(self, prompt_len) -> None:
        super(TokenPrompter_w_pos_TransformerGEN, self).__init__()

        self.prompt = nn.Parameter(torch.randn([1, prompt_len, 768]))

        self.dropout = nn.Dropout(0)
        self.transformer = Transformer(768, 3, 4, 768, 768)

        self.pos_embedding = nn.Parameter(torch.randn(1, prompt_len, 1))

    def forward(self):
        return self.transformer(self.prompt + self.pos_embedding)

