"""
Simulate maglev control
Ported from simulateMaglevControl.m
"""

import numpy as np
from scipy.integrate import solve_ivp
from utils import euler2dcm, dcm2euler
from dynamics import quad_ode_function_hf
from MAGLEV_DIGITALTWIN_PYTHON.additiveController import AdditivePIDController


def simulate_maglev_control(R, S, P):
    """
    Simulates closed-loop control of a maglev quadrotor.
    
    Parameters
    ----------
    R : dict
        Reference structure with the following elements:
        - tVec : (N,) array of uniformly-sampled time offsets from initial time (seconds)
        - rIstar : (N, 3) array of desired CM positions in I frame (meters)
        - vIstar : (N, 3) array of desired CM velocities (meters/sec)
        - aIstar : (N, 3) array of desired CM accelerations (meters/sec^2)
        - xIstar : (N, 3) array of desired body x-axis direction (unit vectors)
    
    S : dict
        Simulation structure with the following elements:
        - oversampFact : Oversampling factor (must be >= 1)
        - state0 : Initial state dict with:
            - r : (3,) position in world frame (meters)
            - e : (3,) Euler angles (radians)
            - v : (3,) velocity (meters/sec)
            - omegaB : (3,) angular rate vector (rad/sec)
        - distMat : (N-1, 3) array of disturbance forces (Newtons)
    
    P : dict
        Parameters structure with:
        - quadParams : QuadParams object
        - constants : Constants object
    
    Returns
    -------
    Q : dict
        Output structure with:
        - tVec : (M,) array of output sample time points
        - state : dict with:
            - rMat : (M, 3) position matrix
            - eMat : (M, 3) Euler angles matrix
            - vMat : (M, 3) velocity matrix
            - omegaBMat : (M, 3) angular rate matrix
            - gaps : (M, 4) gap measurements
            - currents : (M, 4) yoke currents
    """
    # Extract initial state
    s0 = S['state0']
    r0 = s0['r']
    e0 = s0['e']
    v0 = s0['v']
    w0 = s0['omegaB']
    currents0 = np.zeros(4)
    
    R0 = euler2dcm(e0).flatten()
    
    x0 = np.concatenate([r0, v0, R0, w0, currents0])
    
    # Setup simulation parameters
    N = len(R['tVec'])
    dtIn = R['tVec'][1] - R['tVec'][0]
    dt = dtIn / S['oversampFact']
    
    p = {
        'quadParams': P['quadParams'],
        'constants': P['constants']
    }
    
    rl = P['quadParams'].rotor_loc
    
    # Initialize outputs
    tOut = []
    xOut = []
    gapsOut = []
    
    xk = x0
    
    # Create controller instance to maintain state
    controller = AdditivePIDController()
    
    for k in range(N - 1):  # loop through each time step
        tspan = np.arange(S['tVec'][k], S['tVec'][k+1] + dt/2, dt)
        
        # Setup reference for this time step
        Rk = {
            'rIstark': R['rIstar'][k, :],
            'vIstark': R['vIstar'][k, :],
            'aIstark': R['aIstar'][k, :],
            'xIstark': R['xIstar'][k, :]
        }
        
        # Setup state for this time step
        Sk = {
            'statek': {
                'rI': xk[0:3],
                'vI': xk[3:6],
                'RBI': xk[6:15],
                'omegaB': xk[15:18],
                'currents': xk[18:22]
            }
        }
        
        # Compute control voltage with noise
        eak = controller.control(Rk, Sk, P) + np.random.normal(0, 0.01, 4)
        
        # Disturbance for this time step
        dk = S['distMat'][k, :]
        
        # Integrate ODE
        sol = solve_ivp(
            lambda t, X: quad_ode_function_hf(t, X, eak, dk, p),
            [tspan[0], tspan[-1]],
            xk,
            t_eval=tspan,
            method='RK45',
            max_step=dt
        )
        
        tk = sol.t
        xk_traj = sol.y.T  # Transpose to get (time, state) shape
        
        # Calculate gaps for all time points in this segment
        NTK = len(tk)
        gapsk = np.zeros((NTK, 4))
        for q in range(NTK):
            R_q = xk_traj[q, 6:15].reshape(3, 3)
            gapskq = np.abs(xk_traj[q, 2]) - np.array([0, 0, 1]) @ R_q.T @ rl
            gapsk[q, :] = gapskq.flatten()
        
        # Store results (excluding last point to avoid duplication)
        tOut.append(tk[:-1])
        xOut.append(xk_traj[:-1, :])
        gapsOut.append(gapsk[:-1, :])
        
        # Prepare for next iteration
        xk = xk_traj[-1, :]
    
    # Concatenate all segments
    tOut = np.concatenate(tOut)
    xOut = np.vstack(xOut)
    gapsOut = np.vstack(gapsOut)
    
    # Add final point
    tOut = np.append(tOut, tk[-1])
    xOut = np.vstack([xOut, xk_traj[-1, :]])
    gapsOut = np.vstack([gapsOut, gapsk[-1, :]])
    
    M = len(tOut)
    
    # Form Euler angles from rotation matrices
    eMat = np.zeros((M, 3))
    for k in range(M):
        Rk = xOut[k, 6:15].reshape(3, 3)
        ek = dcm2euler(Rk)
        eMat[k, :] = np.real(ek)
    
    # Assemble output structure
    Q = {
        'tVec': tOut,
        'state': {
            'rMat': xOut[:, 0:3],
            'vMat': xOut[:, 3:6],
            'eMat': eMat,
            'omegaBMat': xOut[:, 15:18],
            'gaps': gapsOut,
            'currents': xOut[:, 18:22]
        }
    }
    
    return Q
