import numpy as np
import torch
import poselib


def relative_pose_error(T_0to1, R, t, ignore_gt_t_thr=0.0):
    # angle error between 2 vectors
    t_gt = T_0to1[:3, 3]
    n = np.linalg.norm(t) * np.linalg.norm(t_gt)
    t_err = np.rad2deg(np.arccos(np.clip(np.dot(t, t_gt) / n, -1.0, 1.0)))
    t_err = np.minimum(t_err, 180 - t_err)  # handle E ambiguity
    if np.linalg.norm(t_gt) < ignore_gt_t_thr:  # pure rotation is challenging
        t_err = 0

    # angle error between 2 rotation matrices
    R_gt = T_0to1[:3, :3]
    cos = (np.trace(np.dot(R.T, R_gt)) - 1) / 2
    cos = np.clip(cos, -1.0, 1.0)  # handle numercial errors
    R_err = np.rad2deg(np.abs(np.arccos(cos)))

    return t_err, R_err


def intrinsics_to_camera(K):
    px, py = K[0, 2], K[1, 2]
    fx, fy = K[0, 0], K[1, 1]
    return {
        "model": "PINHOLE",
        "width": int(2 * px),
        "height": int(2 * py),
        "params": [fx, fy, px, py],
    }


def estimate_pose(kpts0, kpts1, K0, K1, thresh, conf=0.99999):
    M, info = poselib.estimate_relative_pose(
        kpts0,
        kpts1,
        intrinsics_to_camera(K0),
        intrinsics_to_camera(K1),
        {"max_epipolar_error": thresh, "success_prob": conf, "min_iterations": 20, "max_iterations": 1_000},
    )

    R, t, inl = M.R, M.t, info["inliers"]
    inl = np.array(inl)
    ret = (R, t, inl)

    return ret


def tensor2bgr(t):
    return (t.cpu()[0].permute(1, 2, 0).numpy() * 255).astype(np.uint8)


def compute_pose_error(match_fn, data):
    result = {}

    with torch.no_grad():
        mkpts0, mkpts1 = match_fn(tensor2bgr(data["image0"]), tensor2bgr(data["image1"]))

    mkpts0 = mkpts0 * data["scale0"].numpy()
    mkpts1 = mkpts1 * data["scale1"].numpy()

    K0, K1 = data["K0"][0].numpy(), data["K1"][0].numpy()
    T_0to1 = data["T_0to1"][0].numpy()
    T_1to0 = data["T_1to0"][0].numpy()

    result = {}
    conf = 0.99999

    ret = estimate_pose(mkpts0, mkpts1, K0, K1, 4.0, conf)
    if ret is not None:
        R, t, inliers = ret
        t_err, R_err = relative_pose_error(T_0to1, R, t, ignore_gt_t_thr=0.0)
        result["R_err"] = R_err
        result["t_err"] = t_err

    return result


def error_auc(errors, thresholds=[5, 10, 20]):
    """
    Args:
        errors (list): [N,]
        thresholds (list)
    """
    errors = [0] + sorted(list(errors))
    recall = list(np.linspace(0, 1, len(errors)))

    aucs = []

    for thr in thresholds:
        last_index = np.searchsorted(errors, thr)
        y = recall[:last_index] + [recall[last_index - 1]]
        x = errors[:last_index] + [thr]
        aucs.append(np.trapz(y, x) / thr)

    return {f"auc@{t}": auc for t, auc in zip(thresholds, aucs)}


def compute_maa(pairs, thresholds=[5, 10, 20]):
    # print("auc / mAcc on %d pairs" % (len(pairs)))
    errors = []

    for p in pairs:
        et = p["t_err"]
        er = p["R_err"]
        errors.append(max(et, er))

    d_err_auc = error_auc(errors)

    # for k,v in d_err_auc.items():
    #     print(k, ': ', '%.1f'%(v*100))

    errors = np.array(errors)

    for t in thresholds:
        acc = (errors <= t).sum() / len(errors)
        # print("mAcc@%d: %.1f "%(t, acc*100))

    return d_err_auc, errors


def homo_trans(coord, H):
    kpt_num = coord.shape[0]
    homo_coord = np.concatenate((coord, np.ones((kpt_num, 1))), axis=-1)
    proj_coord = np.matmul(H, homo_coord.T).T
    proj_coord = proj_coord / proj_coord[:, 2][..., None]
    proj_coord = proj_coord[:, 0:2]
    return proj_coord
