import ruptures as rpt
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from typing import List, Tuple


def find_2d_data_bkps(X: List[Tuple[int, int]]) -> List[int]:
    X_clean = [point if point is not None else (np.nan, np.nan) for point in X]
    X = np.array(X_clean, dtype=float)
    X = pd.DataFrame(X).interpolate("linear").bfill().ffill().to_numpy()
    X_std = StandardScaler().fit_transform(X)
    algo = rpt.KernelCPD(kernel="rbf", jump=1).fit(X_std)
    bkps = algo.predict(pen=10)
    return bkps[:-1]


def get_interval_average_bbox(
    bboxes: List[Tuple[int, int, int, int] | None], bkps: List[int]
) -> List[Tuple[int, int, int, int]]:
    average_bboxes = []
    for left, right in zip(bkps[:-1], bkps[1:]):
        bboxes_interval = bboxes[left:right]
        valid_bboxes = [bbox for bbox in bboxes_interval if bbox is not None]
        if len(valid_bboxes) > 0:
            average_bbox = np.mean(valid_bboxes, axis=0)
            average_bboxes.append(tuple(map(int, average_bbox)))
        else:
            average_bboxes.append(None)
    return average_bboxes


def find_idxs_interval(idxs: List[int], bkps: List[int]) -> List[int]:
    def _find_idx_interval(_idx: int) -> int:
        left = 0
        right = len(bkps) - 2 

        while left <= right:
            mid = (left + right) // 2
            if bkps[mid] <= _idx < bkps[mid + 1]:
                return mid
            elif _idx < bkps[mid]:
                right = mid - 1
            else:
                left = mid + 1
        return min(max(left, 0), len(bkps) - 2)

    intervals = []
    for idx in idxs:
        interval_idx = _find_idx_interval(idx)
        intervals.append(interval_idx)
    return intervals
