import sys
import numpy as np
from PyQt5 import QtWidgets, QtCore
from vispy import scene, app
import sounddevice as sd
from scipy.special import jv, jn_zeros

# -------------------------------
# Grid & φ
# -------------------------------
Nx, Ny = 100, 100
x = np.linspace(-1,1,Nx)
y = np.linspace(-1,1,Ny)
Xg, Yg = np.meshgrid(x,y)
phi = (1 + 5**0.5)/2

# -------------------------------
# Audio player
# -------------------------------
sample_rate = 44100
duration = 0.1
class AudioPlayer:
    def __init__(self):
        self.current_freq = None
        self.playing = False
    def play_tone(self, frequency):
        if self.current_freq != frequency or not self.playing:
            sd.stop()
            self.current_freq = frequency
            self.playing = True
            t = np.linspace(0,duration,int(sample_rate*duration), endpoint=False)
            waveform = 0.2*np.sin(2*np.pi*frequency*t)
            sd.play(waveform, samplerate=sample_rate, loop=True)
    def stop(self):
        self.playing=False
        sd.stop()
audio_player = AudioPlayer()

def note_to_freq(note_val):
    return 220.0 * 2 ** (note_val/12)

# -------------------------------
# Cymatic generators
# -------------------------------
def get_cymatic_params(f_query):
    alpha = 0.5 + 0.5*np.sin(f_query/100)
    beta  = 0.5 + 0.5*np.cos(f_query/200)
    eta   = 0.3 + 0.3*np.sin(f_query/50)
    zeta  = 0.3 + 0.3*np.cos(f_query/70)
    return {"alpha": alpha, "beta": beta, "eta": eta, "zeta": zeta}

def generate_cartesian(X, Y, params, t):
    Xn, Yn = (X+1)/2, (Y+1)/2
    return (np.sin(params["alpha"]*np.pi*Xn + t) * np.sin(params["beta"]*np.pi*Yn + t) +
            params["eta"]*np.cos(params["zeta"]*np.pi*(Xn+Yn) + t))

def choose_modes(f, f0=110, alpha=1, beta=1/phi, gamma=0, max_n=4, max_m=4, top_k=4):
    E = np.log(max(f,1e-12)/f0)/np.log(phi)
    candidates = []
    for n in range(max_n+1):
        for m in range(max_m+1):
            score = abs(alpha*n + beta*m + gamma - E)
            candidates.append(((n,m), score))
    candidates.sort(key=lambda x: x[1])
    return [c[0] for c in candidates[:top_k]]

def generate_polar(X, Y, f_query, params, plate_radius=1.0, t=0):
    R = np.sqrt(X**2 + Y**2)
    Theta = np.arctan2(Y, X)
    U = np.zeros_like(R)
#    mask = (R <= plate_radius)  # full disc
    # or even no mask at all
    mask = np.ones_like(R, dtype=bool)

    modes = choose_modes(f_query)
    for n, m in modes:
        roots = jn_zeros(m, 2)  # keep 2 radial zeros per mode
        for j_idx, root in enumerate(roots):
            scale = phi ** (-j_idx)
            k = root / (plate_radius * scale + 1e-12)
            amp = phi ** (-(params["eta"] * n + params["zeta"] * m + 0.5 * j_idx))
            phase = (n + m + j_idx) * 0.37
            contrib = amp * jv(m, k * R) * np.cos(m * Theta + phase + t)
            contrib = contrib * mask
            U += contrib

    return U


# -------------------------------
# PyQt5 + VisPy
# -------------------------------
class CymaticWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("φ-Tuned Cymatic OpenGL Demo")
        
        # Central widget = VisPy canvas
        self.canvas = scene.SceneCanvas(keys='interactive', bgcolor='black', size=(900,900), show=True)
        self.setCentralWidget(self.canvas.native)
        self.view = self.canvas.central_widget.add_view()
        self.view.camera = scene.TurntableCamera(fov=45, azimuth=30, elevation=30, distance=4)
        
        # Initial surface
        Z = generate_cartesian(Xg, Yg, get_cymatic_params(440), t=0)
        self.surface = scene.visuals.SurfacePlot(x=Xg, y=Yg, z=Z,
                                                 color=(0.3,0.6,1,1),
                                                 shading='smooth',
                                                 parent=self.view.scene)
        
        # Dock for sliders
        dock = QtWidgets.QDockWidget("Controls", self)
        dock_widget = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout()
        
        # Note slider
        self.note_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        self.note_slider.setMinimum(0)
        self.note_slider.setMaximum(72)
        self.note_slider.setValue(36)
        self.note_slider.setTickPosition(QtWidgets.QSlider.TicksBelow)
        self.note_slider.setTickInterval(6)
        layout.addWidget(QtWidgets.QLabel("Note"))
        layout.addWidget(self.note_slider)
        
        # Morph slider
        self.morph_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        self.morph_slider.setMinimum(0)
        self.morph_slider.setMaximum(100)
        self.morph_slider.setValue(0)
        self.morph_slider.setTickPosition(QtWidgets.QSlider.TicksBelow)
        self.morph_slider.setTickInterval(10)
        layout.addWidget(QtWidgets.QLabel("Morph (Cart ↔ Polar)"))
        layout.addWidget(self.morph_slider)
        
        dock_widget.setLayout(layout)
        dock.setWidget(dock_widget)
        self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, dock)
        
        # Animation timer
        self.t = 0.0
        self.timer = app.Timer(interval=1/60, connect=self.update, start=True)
    
    def update(self, event):
        self.t += 0.03
        morph = self.morph_slider.value() / 100.0   # <-- add parentheses
        note_val = self.note_slider.value()         # <-- add parentheses
        f_query = note_to_freq(note_val)
        audio_player.play_tone(f_query)
        params = get_cymatic_params(f_query)
        Z_cart = generate_cartesian(Xg, Yg, params, self.t)
        Z_polar = generate_polar(Xg, Yg, f_query, params, t=self.t)
        Z_new = (1-morph)*Z_cart + morph*Z_polar
        self.surface.set_data(z=Z_new)


# -------------------------------
if __name__ == '__main__':
    app_qt = QtWidgets.QApplication(sys.argv)
    win = CymaticWindow()
    win.show()
    app.run()
    audio_player.stop()
