import numpy as np
import bisect as bs
from riskreg.phi_density_methods import phi_custom

## calculate the phi relevance function
def phi(
    y,                 ## response variable y
    ctrl_pts=None,     ## control points (used in default/manual method)
    method="default"   ## method to use: "default", "manual", "gmm", "kde", "hist", "spectral"
):
    """
    Computes the phi relevance values for a response variable y.

    Parameters:
    - y: Target variable as a pandas Series
    - ctrl_pts: Dictionary of control points (required for 'default' and 'manual')
    - method: Relevance scoring method to use
        - "default": Piecewise cubic spline using auto-generated control points
        - "manual": Piecewise cubic spline using user-defined control points
        - "gmm": Gaussian Mixture Model-based density estimation
        - "kde": Kernel Density Estimation
        - "hist": Histogram-based relevance
        - "spectral": Spectral Density Estimation (heuristic)

    Returns:
    - List of relevance values between 0 and 1
    """
    y = y.reset_index(drop=True)

    if method in ["default", "manual"]:
        if ctrl_pts is None:
            raise ValueError("Control points must be provided for 'default' or 'manual' phi method.")

        n = len(y)
        num_pts = ctrl_pts["num_pts"]
        ctrl_pts = ctrl_pts["ctrl_pts"]

        y_phi = phi_init(y, n, num_pts, ctrl_pts)
        return y_phi
    else:
        return phi_custom(y, method=method)

## pre-process control points and calculate phi values
def phi_init(y, n, num_pts, ctrl_pts):
    x = []
    y_rel = []
    m = []
    
    for i in range(num_pts):
        x.append(ctrl_pts[3 * i])
        y_rel.append(ctrl_pts[3 * i + 1])
        m.append(ctrl_pts[3 * i + 2])
    
    h = []
    delta = []
    for i in range(num_pts - 1):
        h.append(x[i + 1] - x[i])
        delta.append((y_rel[i + 1] - y_rel[i]) / h[i])
    
    m_adj = pchip_slope_mono_fc(m, delta, num_pts)

    a = y_rel
    b = m_adj
    c = []
    d = []
    for i in range(num_pts - 1):
        c.append((3 * delta[i] - 2 * m_adj[i] - m_adj[i + 1]) / h[i])
        d.append((m_adj[i] - 2 * delta[i] + m_adj[i + 1]) / (h[i] * h[i]))

    y_phi = [None] * n
    for i in range(n):
        y_phi[i] = pchip_val(y[i], x, a, b, c, d, num_pts)

    return y_phi

## calculate slopes for shape preserving hermite cubic polynomials
def pchip_slope_mono_fc(m, delta, num_pts):
    for k in range(num_pts - 1):
        sk = delta[k]
        k1 = k + 1

        if abs(sk) == 0:
            m[k] = m[k1] = 0
        else:
            alpha = m[k] / sk
            beta = m[k1] / sk

            if abs(m[k]) != 0 and alpha < 0:
                m[k] = -m[k]
                alpha = m[k] / sk

            if abs(m[k1]) != 0 and beta < 0:
                m[k1] = -m[k1]
                beta = m[k1] / sk

            m_2ab3 = 2 * alpha + beta - 3
            m_a2b3 = alpha + 2 * beta - 3

            if m_2ab3 > 0 and m_a2b3 > 0 and alpha * (m_2ab3 + m_a2b3) < (m_2ab3 * m_2ab3):
                taus = 3 * sk / np.sqrt(alpha * alpha + beta * beta)
                m[k] = taus * alpha
                m[k1] = taus * beta

    return m

## calculate phi values based on monotone piecewise cubic interpolation
def pchip_val(y, x, a, b, c, d, num_pts):
    i = bs.bisect(x, y) - 1

    if i == num_pts - 1:
        y_val = a[i] + b[i] * (y - x[i])
    elif i < 0:
        y_val = 1
    else:
        s = y - x[i]
        y_val = a[i] + s * (b[i] + s * (c[i] + s * d[i]))

    return y_val
