import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

from .geom import rnd_sample, interpolate


class NghSampler2(nn.Module):
    """Similar to NghSampler, but doesnt warp the 2nd image.
    Distance to GT =>  0 ... pos_d ... neg_d ... ngh
    Pixel label    =>  + + + + + + 0 0 - - - - - - -

    Subsample on query side: if > 0, regular grid
                                < 0, random points
    In both cases, the number of query points is = W*H/subq**2
    """

    def __init__(
        self,
        ngh,
        subq=1,
        subd=1,
        pos_d=0,
        neg_d=2,
        border=None,
        maxpool_pos=True,
        subd_neg=0,
    ):
        nn.Module.__init__(self)
        assert 0 <= pos_d < neg_d <= (ngh if ngh else 99)
        self.ngh = ngh
        self.pos_d = pos_d
        self.neg_d = neg_d
        assert subd <= ngh or ngh == 0
        assert subq != 0
        self.sub_q = subq
        self.sub_d = subd
        self.sub_d_neg = subd_neg
        if border is None:
            border = ngh
        assert border >= ngh, "border has to be larger than ngh"
        self.border = border
        self.maxpool_pos = maxpool_pos
        self.precompute_offsets()

    def precompute_offsets(self):
        pos_d2 = self.pos_d**2
        neg_d2 = self.neg_d**2
        rad2 = self.ngh**2
        rad = (self.ngh // self.sub_d) * self.ngh  # make an integer multiple
        pos = []
        neg = []
        for j in range(-rad, rad + 1, self.sub_d):
            for i in range(-rad, rad + 1, self.sub_d):
                d2 = i * i + j * j
                if d2 <= pos_d2:
                    pos.append((i, j))
                elif neg_d2 <= d2 <= rad2:
                    neg.append((i, j))

        self.register_buffer("pos_offsets", torch.LongTensor(pos).view(-1, 2).t())
        self.register_buffer("neg_offsets", torch.LongTensor(neg).view(-1, 2).t())

    def gen_grid(self, step, B, H, W, dev):
        b1 = torch.arange(B, device=dev)
        if step > 0:
            # regular grid
            x1 = torch.arange(self.border, W - self.border, step, device=dev)
            y1 = torch.arange(self.border, H - self.border, step, device=dev)
            H1, W1 = len(y1), len(x1)
            x1 = x1[None, None, :].expand(B, H1, W1).reshape(-1)
            y1 = y1[None, :, None].expand(B, H1, W1).reshape(-1)
            b1 = b1[:, None, None].expand(B, H1, W1).reshape(-1)
            shape = (B, H1, W1)
        else:
            # randomly spread
            n = (H - 2 * self.border) * (W - 2 * self.border) // step**2
            x1 = torch.randint(self.border, W - self.border, (n,), device=dev)
            y1 = torch.randint(self.border, H - self.border, (n,), device=dev)
            x1 = x1[None, :].expand(B, n).reshape(-1)
            y1 = y1[None, :].expand(B, n).reshape(-1)
            b1 = b1[:, None].expand(B, n).reshape(-1)
            shape = (B, n)
        return b1, y1, x1, shape

    def forward(self, feat0, feat1, conf0, conf1, pos0, pos1, B, H, W, N=2500):
        pscores_ls, nscores_ls, distractors_ls = [], [], []
        valid_feat0_ls = []
        valid_pos1_ls, valid_pos2_ls = [], []
        qconf_ls = []
        mask_ls = []

        for i in range(B):
            # positions in the first image
            tmp_mask = (
                (pos0[i][:, 1] >= self.border)
                * (pos0[i][:, 1] < W - self.border)
                * (pos0[i][:, 0] >= self.border)
                * (pos0[i][:, 0] < H - self.border)
            )

            selected_pos0 = pos0[i][tmp_mask]
            selected_pos1 = pos1[i][tmp_mask]
            valid_pos0, valid_pos1 = rnd_sample([selected_pos0, selected_pos1], N)

            # sample features from first image
            valid_feat0 = interpolate(valid_pos0 / 4, feat0[i])  # [N, 128]
            valid_feat0 = F.normalize(valid_feat0, p=2, dim=-1)  # [N, 128]
            qconf = interpolate(valid_pos0 / 4, conf0[i])

            # sample GT from second image
            mask = (
                (valid_pos1[:, 1] >= 0)
                * (valid_pos1[:, 1] < W)
                * (valid_pos1[:, 0] >= 0)
                * (valid_pos1[:, 0] < H)
            )

            def clamp(xy):
                xy = xy
                torch.clamp(xy[0], 0, H - 1, out=xy[0])
                torch.clamp(xy[1], 0, W - 1, out=xy[1])
                return xy

            # compute positive scores
            valid_pos1p = clamp(
                valid_pos1.t()[:, None, :]
                + self.pos_offsets[:, :, None].to(valid_pos1.device)
            )  # [2, 29, N]
            valid_pos1p = valid_pos1p.permute(1, 2, 0).reshape(
                -1, 2
            )  # [29, N, 2] -> [29*N, 2]
            valid_feat1p = interpolate(valid_pos1p / 4, feat1[i]).reshape(
                self.pos_offsets.shape[-1], -1, 128
            )  # [29, N, 128]
            valid_feat1p = F.normalize(valid_feat1p, p=2, dim=-1)  # [29, N, 128]

            pscores = (
                (valid_feat0[None, :, :] * valid_feat1p).sum(dim=-1).t()
            )  # [N, 29]
            pscores, pos = pscores.max(dim=1, keepdim=True)
            sel = clamp(
                valid_pos1.t() + self.pos_offsets[:, pos.view(-1)].to(valid_pos1.device)
            )
            qconf = (qconf + interpolate(sel.t() / 4, conf1[i])) / 2

            # compute negative scores
            valid_pos1n = clamp(
                valid_pos1.t()[:, None, :]
                + self.neg_offsets[:, :, None].to(valid_pos1.device)
            )  # [2, 29, N]
            valid_pos1n = valid_pos1n.permute(1, 2, 0).reshape(
                -1, 2
            )  # [29, N, 2] -> [29*N, 2]
            valid_feat1n = interpolate(valid_pos1n / 4, feat1[i]).reshape(
                self.neg_offsets.shape[-1], -1, 128
            )  # [29, N, 128]
            valid_feat1n = F.normalize(valid_feat1n, p=2, dim=-1)  # [29, N, 128]
            nscores = (
                (valid_feat0[None, :, :] * valid_feat1n).sum(dim=-1).t()
            )  # [N, 29]

            if self.sub_d_neg:
                valid_pos2 = rnd_sample([selected_pos1], N)[0]
                distractors = interpolate(valid_pos2 / 4, feat1[i])
                distractors = F.normalize(distractors, p=2, dim=-1)

            pscores_ls.append(pscores)
            nscores_ls.append(nscores)
            distractors_ls.append(distractors)
            valid_feat0_ls.append(valid_feat0)
            valid_pos1_ls.append(valid_pos1)
            valid_pos2_ls.append(valid_pos2)
            qconf_ls.append(qconf)
            mask_ls.append(mask)

        N = np.min([len(i) for i in qconf_ls])

        # merge batches
        qconf = torch.stack([i[:N] for i in qconf_ls], dim=0).squeeze(-1)
        mask = torch.stack([i[:N] for i in mask_ls], dim=0)
        pscores = torch.cat([i[:N] for i in pscores_ls], dim=0)
        nscores = torch.cat([i[:N] for i in nscores_ls], dim=0)
        distractors = torch.cat([i[:N] for i in distractors_ls], dim=0)
        valid_feat0 = torch.cat([i[:N] for i in valid_feat0_ls], dim=0)
        valid_pos1 = torch.cat([i[:N] for i in valid_pos1_ls], dim=0)
        valid_pos2 = torch.cat([i[:N] for i in valid_pos2_ls], dim=0)

        dscores = torch.matmul(valid_feat0, distractors.t())
        dis2 = (valid_pos2[:, 1] - valid_pos1[:, 1][:, None]) ** 2 + (
            valid_pos2[:, 0] - valid_pos1[:, 0][:, None]
        ) ** 2
        b = torch.arange(B, device=dscores.device)[:, None].expand(B, N).reshape(-1)
        dis2 += (b != b[:, None]).long() * self.neg_d**2
        dscores[dis2 < self.neg_d**2] = 0
        scores = torch.cat((pscores, nscores, dscores), dim=1)

        gt = scores.new_zeros(scores.shape, dtype=torch.uint8)
        gt[:, : pscores.shape[1]] = 1

        return scores, gt, mask, qconf