import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
import sounddevice as sd
import threading

# -------------------------------
# Synthetic training data
# -------------------------------
frequencies = np.linspace(100, 1000, 100)
nodal_counts = 3*np.sin(0.01*frequencies) + 0.5*np.cos(0.005*frequencies) + np.random.normal(0,0.05,len(frequencies))

X = np.vstack([
    frequencies,
    frequencies**2,
    np.sin(frequencies/100),
    np.cos(frequencies/200),
    np.power(1.618, frequencies/500),  # φ-scaling
    np.sqrt(frequencies),
    np.log1p(frequencies)
]).T

Y = np.vstack([
    nodal_counts + 0.1*np.random.randn(len(frequencies)),
    nodal_counts*2 + 0.2*np.random.randn(len(frequencies)),
    nodal_counts*0.7 + 0.1*np.random.randn(len(frequencies)),
    nodal_counts*1.1 + 0.2*np.random.randn(len(frequencies))
]).T

X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=42)
model = LinearRegression().fit(X_train, Y_train)

print("Learned T matrix:\n", model.coef_)
print("Intercept:\n", model.intercept_)
print("R^2 test score:", model.score(X_test, Y_test))

# -------------------------------
# φ-hash tuning
# -------------------------------
def safe_phi_hash(x, phi=1.6180339887):
    x_mod = np.mod(x, 1000)
    return np.mod(np.power(phi, x_mod), 1.0)

def conditional_phi_tune(y_pred, x, y_true, lam=0.01):
    hash_vec = safe_phi_hash(x)
    hash_mapped = np.ones_like(y_pred) * np.sum(hash_vec)/len(hash_vec)
    y_candidate = y_pred + lam*hash_mapped
    if np.linalg.norm(y_candidate - y_true) < np.linalg.norm(y_pred - y_true):
        return y_candidate
    return y_pred

# -------------------------------
# Cymatic params
# -------------------------------
def get_cymatic_params(f_query):
    features = np.array([
        f_query,
        f_query**2,
        np.sin(f_query/100),
        np.cos(f_query/200),
        np.power(1.618, f_query/500),
        np.sqrt(f_query),
        np.log1p(f_query)
    ]).reshape(1,-1)
    params = model.predict(features)[0]
    y_true_ref = Y_test.mean(axis=0)
    params_tuned = conditional_phi_tune(params, features.flatten(), y_true_ref)
    return {
        "alpha": abs(params_tuned[0]),
        "beta": abs(params_tuned[1]),
        "eta": abs(params_tuned[2]),
        "zeta": abs(params_tuned[3])
    }

# -------------------------------
# Coordinate morphing
# -------------------------------
def morph_coords(Xg, Yg, R, Theta, morph_val):
    # morph_val 0=Cartesian, 1=Polar
    Xm = (1-morph_val)*Xg + morph_val*R*np.cos(Theta)
    Ym = (1-morph_val)*Yg + morph_val*R*np.sin(Theta)
    return Xm, Ym

def cymatic(coords, alpha,beta,eta,zeta):
    Xc,Yc = coords
    return np.sin(alpha*np.pi*Xc) * np.sin(beta*np.pi*Yc) + eta*np.cos(zeta*np.pi*(Xc+Yc))

# -------------------------------
# Audio playback
# -------------------------------
sample_rate = 44100
duration = 1.0  # seconds

def note_to_freq(note_val):
    return 220.0 * 2**(note_val/12)  # A3=220Hz, 12-EDO scaling

def play_tone(frequency):
    t = np.linspace(0, duration, int(sample_rate*duration), endpoint=False, dtype=np.float32)
    waveform = (0.2 * np.sin(2*np.pi*frequency*t)).astype(np.float32)
    sd.play(waveform, samplerate=sample_rate)

# -------------------------------
# Prepare grid
# -------------------------------
Nx, Ny = 300, 300
x = np.linspace(0,1,Nx)
y = np.linspace(0,1,Ny)
Xg, Yg = np.meshgrid(x,y)
R = np.sqrt(Xg**2 + Yg**2)
Theta = np.arctan2(Yg,Xg)

# -------------------------------
# Plot + sliders
# -------------------------------
fig, ax = plt.subplots(figsize=(6,6))
plt.subplots_adjust(bottom=0.25)

morph_slider_ax = plt.axes([0.25,0.1,0.65,0.03])
morph_slider = Slider(morph_slider_ax, "Morph", 0.0,1.0,valinit=0.0)

note_slider_ax = plt.axes([0.25,0.05,0.65,0.03])
note_slider = Slider(note_slider_ax, "Note", 0.0,72.0,valinit=0.0)

# Initial draw
params = get_cymatic_params(note_to_freq(note_slider.val))
Z = cymatic((Xg,Yg), **params)
cont = ax.contour(Xg,Yg,Z, levels=[0], colors='black')

def update(val):
    morph_val = morph_slider.val
    note_val = note_slider.val
    f_query = note_to_freq(note_val)
    params = get_cymatic_params(f_query)

    Xm, Ym = morph_coords(Xg,Yg,R,Theta,morph_val)
    Z = cymatic((Xm,Ym), **params)

    # Remove previous contours
    for coll in ax.collections:
        coll.remove()

    ax.contour(Xm,Ym,Z, levels=[0], colors='black')
    ax.set_title(f"Note: {note_val:.1f}, Freq: {f_query:.2f} Hz")
    fig.canvas.draw_idle()

    threading.Thread(target=play_tone, args=(f_query,), daemon=True).start()

morph_slider.on_changed(update)
note_slider.on_changed(update)

plt.show()
