import cv2
import numpy as np
from scipy.spatial.distance import cdist
from scipy.optimize import linear_sum_assignment


class SimpleAffineTransform:
    def __init__(self, translation=(0, 0), scale=1.0):
        self.translation = np.array(translation)
        self.scale = scale

    def estimate(self, src, dst):
        src_center = np.mean(src, axis=0)
        dst_center = np.mean(dst, axis=0)
        self.translation = dst_center - src_center
        src_dists = np.linalg.norm(src - src_center, axis=1)
        dst_dists = np.linalg.norm(dst - dst_center, axis=1)
        self.scale = np.mean(dst_dists) / (np.mean(src_dists) + 1e-10)

    def inverse(self):
        return SimpleAffineTransform(-self.translation, 1.0 / self.scale)

    def __call__(self, coords):
        return (
            self.scale * (coords - np.mean(coords, axis=0))
            + np.mean(coords, axis=0)
            + self.translation
        )

    def residuals(self, src, dst):
        return np.sqrt(np.sum((self(src) - dst) ** 2, axis=1))


def norm_coords(x, left, right):
    if x < left:
        return left
    if x > right:
        return right
    return x


def norm_same_token(token):
    special_map = {
        "\\dot": ".",
        "\\Dot": ".",
        "\\cdot": ".",
        "\\cdotp": ".",
        "\\ldotp": ".",
        "\\mid": "|",
        "\\rightarrow": "\\to",
        "\\top": "T",
        "\\Tilde": "\\tilde",
        "\\prime": "'",
        "\\ast": "*",
        "\\left<": "\\langle",
        "\\right>": "\\rangle",
        "\\lbrace": "\{",
        "\\rbrace": "\}",
        "\\lbrack": "[",
        "\\rbrack": "]",
        "\\blackslash": "/",
        "\\slash": "/",
        "\\leq": "\\le",
        "\\geq": "\\ge",
        "\\neq": "\\ne",
        "\\Vert": "\\|",
        "\\lVert": "\\|",
        "\\rVert": "\\|",
        "\\vert": "|",
        "\\lvert": "|",
        "\\rvert": "|",
        "\\colon": ":",
        "\\Ddot": "\\ddot",
        "\\Bar": "\\bar",
        "\\Vec": "\\vec",
        "\\parallel": "\\|",
        "\\dag": "\\dagger",
        "\\ddag": "\\ddagger",
        "\\textlangle": "<",
        "\\textrangle": ">",
        "\\textgreater": ">",
        "\\textless": "<",
        "\\textbackslash": "\\",
        "\\textunderscore": "_",
        "\\=": "=",
        "\\neg": "\\lnot",
        "\\neq": "\\not=",
    }
    if token.startswith("\\left") or token.startswith("\\right"):
        if (
            "arrow" not in token
            and "<" not in token
            and ">" not in token
            and "harpoon" not in token
        ):
            token = token.replace("\\left", "").replace("\\right", "")
    if token.startswith("\\big") or token.startswith("\\Big"):
        if "\\" in token[4:]:
            token = "\\" + token[4:].split("\\")[-1]
        else:
            token = token[-1]
    if token in special_map.keys():
        token = special_map[token]
    if token.startswith("\\wide"):
        return token.replace("wide", "")
    if token.startswith("\\var"):
        return token.replace("var", "")
    if token.startswith("\\string"):
        return token.replace("\\string", "")
    return token


class HungarianMatcher:
    def __init__(
        self,
        cost_token: float = 1,
        cost_position: float = 0.05,
        cost_order: float = 0.15,
    ):
        self.cost_token = cost_token
        self.cost_position = cost_position
        self.cost_order = cost_order
        self.cost = {}

    def calculate_token_cost(self, box_gt, box_pred):

        all_tokens = [data["token"] for data in box_gt + box_pred]
        unique_tokens = sorted(list(set(all_tokens)))
        token2id = {token: i for i, token in enumerate(unique_tokens)}
        num_classes = len(token2id)

        all_norm_tokens = [norm_same_token(data["token"]) for data in box_gt + box_pred]
        unique_norm_tokens = sorted(list(set(all_norm_tokens)))
        token2id_norm = {token: i for i, token in enumerate(unique_norm_tokens)}
        num_classes_norm = len(token2id_norm)

        gt_token_array = np.array([token2id[data["token"]] for data in box_gt])
        norm_gt_token_array = np.array(
            [token2id_norm[norm_same_token(data["token"])] for data in box_gt]
        )

        pred_token_logits = np.zeros((len(box_pred), num_classes))
        for i, data in enumerate(box_pred):
            if data["token"] in token2id:
                pred_token_logits[i, token2id[data["token"]]] = 1

        norm_pred_token_logits = np.zeros((len(box_pred), num_classes_norm))
        for i, data in enumerate(box_pred):
            norm_token = norm_same_token(data["token"])
            if norm_token in token2id_norm:
                norm_pred_token_logits[i, token2id_norm[norm_token]] = 1

        if gt_token_array.size == 0 or pred_token_logits.shape[0] == 0:
            return np.empty((len(box_gt), len(box_pred)))

        token_cost = 1.0 - pred_token_logits[:, gt_token_array]
        norm_token_cost = 1.0 - norm_pred_token_logits[:, norm_gt_token_array]

        token_cost[np.logical_and(token_cost == 1, norm_token_cost == 0)] = 0.005
        return token_cost.T

    def box2array(self, box_list, size):
        W, H = size
        box_array = []
        for box in box_list:
            x_min, y_min, x_max, y_max = box["bbox"]
            box_array.append(
                [
                    (x_min + x_max) / (2 * W),
                    (y_min + y_max) / (2 * H),
                    (x_max - x_min) / W,
                    (y_max - y_min) / H,
                ]
            )
        return np.array(box_array)

    def order2array(self, box_list, max_token_lens=None):
        if not max_token_lens:
            max_token_lens = len(box_list)
        return np.array([[idx / max_token_lens] for idx, _ in enumerate(box_list)])

    def calculate_l1_cost(self, gt_array, pred_array):
        if gt_array.shape[0] == 0 or pred_array.shape[0] == 0:
            return np.empty((gt_array.shape[0], pred_array.shape[0]))
        return cdist(gt_array, pred_array, "minkowski", p=1) / gt_array.shape[-1]

    def __call__(self, box_gt, box_pred, gt_size, pred_size):
        if not box_gt or not box_pred:
            return []
        gt_box_array = self.box2array(box_gt, gt_size)
        pred_box_array = self.box2array(box_pred, pred_size)
        max_token_lens = max(len(box_gt), len(box_pred))
        gt_order_array = self.order2array(box_gt, max_token_lens)
        pred_order_array = self.order2array(box_pred, max_token_lens)
        token_cost = self.calculate_token_cost(box_gt, box_pred)
        position_cost = self.calculate_l1_cost(gt_box_array, pred_box_array)
        order_cost = self.calculate_l1_cost(gt_order_array, pred_order_array)
        self.cost = {
            "token": token_cost,
            "position": position_cost,
            "order": order_cost,
        }
        cost = (
            self.cost_token * token_cost
            + self.cost_position * position_cost
            + self.cost_order * order_cost
        )
        cost[np.isnan(cost) | np.isinf(cost)] = 100
        row_ind, col_ind = linear_sum_assignment(cost)
        return list(zip(row_ind, col_ind))


def update_inliers(ori_inliers, sub_inliers):
    inliers = np.copy(ori_inliers)
    sub_idx = -1
    for idx in range(len(ori_inliers)):
        if ori_inliers[idx] == False:
            sub_idx += 1
            if sub_inliers[sub_idx] == True:
                inliers[idx] = True
    return inliers

