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


def get_anchors(bases, aspect_ratios):
    anchors = []
    for b in bases:
        for asp in aspect_ratios:
            d, h, w = b * asp[0], b * asp[1], b * asp[2]
            anchors.append([d, h, w])

    return anchors


def box_transform_numpy(windows, targets, weight):
    """
    Calculate regression terms, dz, dy, dx, dd, dh, dw
    # windows should equal to # targets
    windows: [num_window, z, y, x, D, H, W]
    targets: [num_target, z, y, x, D, H, W]
    """
    wz, wy, wx, wd, wh, ww = weight
    bz, by, bx = windows[:, 0], windows[:, 1], windows[:, 2]
    bd, bh, bw = windows[:, 3], windows[:, 4], windows[:, 5]
    tz, ty, tx = targets[:, 0], targets[:, 1], targets[:, 2]
    td, th, tw = targets[:, 3], targets[:, 4], targets[:, 5]

    dz = wz * (tz - bz) / bd
    dy = wy * (ty - by) / bh
    dx = wx * (tx - bx) / bw
    dd = wd * np.log(td / bd)
    dh = wh * np.log(th / bh)
    dw = ww * np.log(tw / bw)

    deltas = np.vstack((dz, dy, dx, dd, dh, dw)).transpose()
    return deltas


def box_transform(windows, targets, weight):
    """
    Calculate regression terms, dz, dy, dx, dd, dh, dw
    # windows should equal to # targets
    windows: [num_window, z, y, x, D, H, W]
    targets: [num_target, z, y, x, D, H, W]
    """
    wz, wy, wx, wd, wh, ww = weight
    bz, by, bx = windows[:, 0], windows[:, 1], windows[:, 2]
    bd, bh, bw = windows[:, 3], windows[:, 4], windows[:, 5]
    tz, ty, tx = targets[:, 0], targets[:, 1], targets[:, 2]
    td, th, tw = targets[:, 3], targets[:, 4], targets[:, 5]

    dz = wz * (tz - bz) / bd
    dy = wy * (ty - by) / bh
    dx = wx * (tx - bx) / bw
    dd = wd * torch.log(td / bd)
    dh = wh * torch.log(th / bh)
    dw = ww * torch.log(tw / bw)

    deltas = torch.stack((dz, dy, dx, dd, dh, dw), dim=0)
    return deltas


def box_transform_inv_numpy(windows, deltas, weight):
    """
    Apply regression terms to predicted bboxes
    windows: [num_window, z, y, x, D, H, W]
    targets: [num_target, z, y, x, D, H, W]
    """
    num = len(windows)
    wz, wy, wx, wd, wh, ww = weight
    predictions = np.zeros((num, 6), dtype=np.float32)

    bz, by, bx = windows[:, 0], windows[:, 1], windows[:, 2]
    bd, bh, bw = windows[:, 3], windows[:, 4], windows[:, 5]
    bz = bz[:, np.newaxis]
    by = by[:, np.newaxis]
    bx = bx[:, np.newaxis]
    bd = bd[:, np.newaxis]
    bh = bh[:, np.newaxis]
    bw = bw[:, np.newaxis]

    dz = deltas[:, 0::6] / wz
    dy = deltas[:, 1::6] / wy
    dx = deltas[:, 2::6] / wx
    dd = deltas[:, 3::6] / wd
    dh = deltas[:, 4::6] / wh
    dw = deltas[:, 5::6] / ww

    z = dz * bd + bz
    y = dy * bh + by
    x = dx * bw + bx

    d = np.exp(dd) * bd
    h = np.exp(dh) * bh
    w = np.exp(dw) * bw

    predictions[:, 0::6] = z
    predictions[:, 1::6] = y
    predictions[:, 2::6] = x
    predictions[:, 3::6] = d
    predictions[:, 4::6] = h
    predictions[:, 5::6] = w

    return predictions


def box_transform_inv(windows, deltas, weight):
    """
    Apply regression terms to predicted bboxes
    windows: [num_window, z, y, x, D, H, W]
    targets: [num_target, z, y, x, D, H, W]
    return:
    prediction: [x1,y1,x2,y2,z1,z2]
    """
    wz, wy, wx, wd, wh, ww = weight

    bz = torch.unsqueeze(windows[:, 0], 1)
    by = torch.unsqueeze(windows[:, 1], 1)
    bx = torch.unsqueeze(windows[:, 2], 1)
    bd = torch.unsqueeze(windows[:, 3], 1)
    bh = torch.unsqueeze(windows[:, 4], 1)
    bw = torch.unsqueeze(windows[:, 5], 1)

    dz = torch.unsqueeze(deltas[:, 0] / wz, 1)
    dy = torch.unsqueeze(deltas[:, 1] / wy, 1)
    dx = torch.unsqueeze(deltas[:, 2] / wx, 1)
    dd = torch.unsqueeze(deltas[:, 3] / wd, 1)
    dh = torch.unsqueeze(deltas[:, 4] / wh, 1)
    dw = torch.unsqueeze(deltas[:, 5] / ww, 1)

    z = dz * bd + bz
    y = dy * bh + by
    x = dx * bw + bx

    d = torch.exp(dd) * bd
    h = torch.exp(dh) * bh
    w = torch.exp(dw) * bw

    predictions = torch.cat((z, y, x, d, h, w), dim=1)

    return predictions


def convert_zyxdhw(bbox):
    # xyxyzz -> zyxdhw
    z = (bbox[:, 4] + bbox[:, 5]) / 2
    y = (bbox[:, 1] + bbox[:, 3]) / 2
    x = (bbox[:, 0] + bbox[:, 2]) / 2
    d = bbox[:, 5] - bbox[:, 4]
    h = bbox[:, 3] - bbox[:, 1]
    w = bbox[:, 2] - bbox[:, 0]

    z = torch.unsqueeze(z, 1)
    y = torch.unsqueeze(y, 1)
    x = torch.unsqueeze(x, 1)
    d = torch.unsqueeze(d, 1)
    h = torch.unsqueeze(h, 1)
    w = torch.unsqueeze(w, 1)

    new_bbox = torch.cat((z, y, x, d, h, w), dim=1)

    return new_bbox


def convert_xyxyzz(bbox):
    # zyxdhw -> xyxyzz
    x1 = bbox[:, 2] - bbox[:, 5] / 2
    y1 = bbox[:, 1] - bbox[:, 4] / 2
    x2 = bbox[:, 2] + bbox[:, 5] / 2
    y2 = bbox[:, 1] + bbox[:, 4] / 2
    z1 = bbox[:, 0] - bbox[:, 3] / 2
    z2 = bbox[:, 0] + bbox[:, 3] / 2

    x1 = torch.unsqueeze(x1, 1)
    x2 = torch.unsqueeze(x2, 1)
    y1 = torch.unsqueeze(y1, 1)
    y2 = torch.unsqueeze(y2, 1)
    z1 = torch.unsqueeze(z1, 1)
    z2 = torch.unsqueeze(z2, 1)

    new_bbox = torch.cat((x1, y1, x2, y2, z1, z2), dim=1)

    return new_bbox


def clip_boxes_numpy(boxes, img_size):
    """
    clip boxes outside the image, all box follows [p, z, y, x, d, h, w]
    """
    depth, height, width = img_size
    boxes[:, 0] = np.clip(boxes[:, 0], 0, depth - 1)
    boxes[:, 1] = np.clip(boxes[:, 1], 0, height - 1)
    boxes[:, 2] = np.clip(boxes[:, 2], 0, width - 1)

    return boxes


@torch.jit.script
def clip_boxes(boxes, img):
    """
    clip boxes outside the image, all box follows [z, y, x, d, h, w]
    """
    _, _, depth, height, width = img.shape
    boxes[:, 0] = torch.clamp(boxes[:, 0], 0, depth - 1)
    boxes[:, 1] = torch.clamp(boxes[:, 1], 0, height - 1)
    boxes[:, 2] = torch.clamp(boxes[:, 2], 0, width - 1)

    return boxes


def convert_to_roi_format(proposals):
    # bpzyxdhw -> bxyxyzz
    b = proposals[:, 0]
    x1 = proposals[:, 4] - proposals[:, 7] / 2
    y1 = proposals[:, 3] - proposals[:, 6] / 2
    x2 = proposals[:, 4] + proposals[:, 7] / 2
    y2 = proposals[:, 3] + proposals[:, 6] / 2
    z1 = proposals[:, 2] - proposals[:, 5] / 2
    z2 = proposals[:, 2] + proposals[:, 5] / 2

    b = torch.unsqueeze(b, 1)
    x1 = torch.unsqueeze(x1, 1)
    x2 = torch.unsqueeze(x2, 1)
    y1 = torch.unsqueeze(y1, 1)
    y2 = torch.unsqueeze(y2, 1)
    z1 = torch.unsqueeze(z1, 1)
    z2 = torch.unsqueeze(z2, 1)

    rois = torch.cat((b, x1, y1, x2, y2, z1, z2), dim=1)

    return rois


@torch.jit.script
def pad2factor(image, factor=torch.tensor(16.0), pad_value=torch.tensor(-300)):
    _, _, depth, height, width = image.shape
    d = torch.ceil(depth / factor) * factor
    h = torch.ceil(height / factor) * factor
    w = torch.ceil(width / factor) * factor
    pad = [0, int(w - width), 0, int(h - height), 0, int(d - depth)]
    # pad = [0, int(256 - width), 0, int(192 - height), 0, int(256 - depth)]
    image = F.pad(image, pad, 'constant', value=pad_value)
    return image


def norm(image, hu_min=-300.0, hu_max=1000.0):
    image = (torch.clamp(image.half(), hu_min, hu_max) - hu_min) / float(hu_max - hu_min)
    return (image - 0.5) * 2.0


@torch.jit.script
def add_rcnn_probability(rpn_proposals, logits):
    probs = F.softmax(logits, dim=1)
    p_rcnn = probs[:, 1].reshape(-1, 1)
    rpn_proposals = torch.cat([rpn_proposals, p_rcnn], dim=1)

    return rpn_proposals
