

'''
calculate the velocity of the imu.

The measured acc is in imu body frame.

1. acc offset along each axis calculation.
    - gravity mapped to imu body frame, according to rpy;
    - subtract gravity items to get the pure acc along each axis;
    - keep imu static, average the pure acc values, as the offset;
    - calibrate the offset in following use.

2. vel calculation.
    - get calibrated acc in imu body frame;
    - convert this calibrate acc to world frame;
    - integtate this acc, get the vel in world frame;
    - convert the vel back to imu body frame.
'''


import math


G = 9.80665


def mat_vec_mul(R, v):
    return [
        R[0][0] * v[0] + R[0][1] * v[1] + R[0][2] * v[2],
        R[1][0] * v[0] + R[1][1] * v[1] + R[1][2] * v[2],
        R[2][0] * v[0] + R[2][1] * v[1] + R[2][2] * v[2],
    ]


def mat_transpose(R):
    return [
        [R[0][0], R[1][0], R[2][0]],
        [R[0][1], R[1][1], R[2][1]],
        [R[0][2], R[1][2], R[2][2]],
    ]


def rotation_matrix_body_to_world(rpy_deg):
    """
    rpy_deg = [roll, pitch, yaw], degree

    Convention:
        roll  around X
        pitch around Y
        yaw   around Z

    R_body_to_world = Rz(yaw) @ Ry(pitch) @ Rx(roll)
    """

    roll = math.radians(rpy_deg[0])
    pitch = math.radians(rpy_deg[1])
    yaw = math.radians(rpy_deg[2])

    cr, sr = math.cos(roll), math.sin(roll)
    cp, sp = math.cos(pitch), math.sin(pitch)
    cy, sy = math.cos(yaw), math.sin(yaw)

    return [
        [cy * cp, cy * sp * sr - sy * cr, cy * sp * cr + sy * sr],
        [sy * cp, sy * sp * sr + cy * cr, sy * sp * cr - cy * sr],
        [-sp,     cp * sr,                  cp * cr],
    ]


class IMUVelocityEstimator:
    def __init__(self):
        # Velocity is stored in world frame, unit m/s
        self.vel_world = [0.0, 0.0, 0.0]

        # Acceleration offset in body frame, unit g
        self.acc_offset_body = [0.0, 0.0, 0.0]

    def reset_velocity(self):
        self.vel_world = [0.0, 0.0, 0.0]

    def set_acc_offset_body(self, acc_offset_body):
        self.acc_offset_body = list(acc_offset_body)

    def update(self, acc_body_g, rpy_deg, dt):
        """
        Parameters
        ----------
        acc_body_g:
            measured acceleration in IMU/body frame, unit g

        rpy_deg:
            [roll, pitch, yaw], unit degree

        dt:
            time step, unit second

        Returns
        -------
        acc_world_linear:
            gravity-compensated acceleration in world frame, unit m/s^2

        vel_world:
            velocity in world frame, unit m/s

        vel_body:
            velocity projected onto current IMU/body frame, unit m/s
        """

        R_bw = rotation_matrix_body_to_world(rpy_deg)
        R_wb = mat_transpose(R_bw)

        # 1. Remove body-frame acceleration offset
        acc_body_corrected_g = [
            acc_body_g[0] - self.acc_offset_body[0],
            acc_body_g[1] - self.acc_offset_body[1],
            acc_body_g[2] - self.acc_offset_body[2],
        ]

        # 2. Convert body acceleration from g to m/s^2
        acc_body_mps2 = [
            acc_body_corrected_g[0] * G,
            acc_body_corrected_g[1] * G,
            acc_body_corrected_g[2] * G,
        ]

        # 3. Convert acceleration body -> world
        acc_world_mps2 = mat_vec_mul(R_bw, acc_body_mps2)

        # 4. Subtract gravity in world frame
        # World Z is upward, and static flat IMU gives +1g on Z.
        acc_world_linear = [
            acc_world_mps2[0],
            acc_world_mps2[1],
            acc_world_mps2[2] - G,
        ]

        # 5. Integrate velocity in world frame
        self.vel_world[0] += acc_world_linear[0] * dt
        self.vel_world[1] += acc_world_linear[1] * dt
        self.vel_world[2] += acc_world_linear[2] * dt

        # 6. Convert velocity world -> current body
        vel_body = mat_vec_mul(R_wb, self.vel_world)

        return acc_world_linear, list(self.vel_world), vel_body

    def calibrate_acc_offset(self, samples):
        """
        Calibrate acceleration offset.

        Parameters
        ----------
        samples:
            list of samples.
            Each sample should be:
                (acc_body_g, rpy_deg)

            acc_body_g: [Ax, Ay, Az], unit g
            rpy_deg:    [roll, pitch, yaw], degree

        Principle
        ---------
        During static calibration:

            measured_acc_body = gravity_body + offset_body

        So:

            offset_body = measured_acc_body - gravity_body

        We average offset_body over all samples.

        Returns
        -------
        acc_offset_body:
            acceleration offset in body frame, unit g
        """

        if len(samples) == 0:
            raise ValueError("samples is empty.")

        offset_sum = [0.0, 0.0, 0.0]

        for acc_body_g, rpy_deg in samples:
            R_bw = rotation_matrix_body_to_world(rpy_deg)
            R_wb = mat_transpose(R_bw)

            # Gravity in world frame.
            # Unit g.
            gravity_world_g = [0.0, 0.0, 1.0]

            # Convert gravity world -> body
            gravity_body_g = mat_vec_mul(R_wb, gravity_world_g)

            # Offset = measured acceleration - expected gravity
            offset_body_g = [
                acc_body_g[0] - gravity_body_g[0],
                acc_body_g[1] - gravity_body_g[1],
                acc_body_g[2] - gravity_body_g[2],
            ]

            offset_sum[0] += offset_body_g[0]
            offset_sum[1] += offset_body_g[1]
            offset_sum[2] += offset_body_g[2]

        n = len(samples)

        self.acc_offset_body = [
            offset_sum[0] / n,
            offset_sum[1] / n,
            offset_sum[2] / n,
        ]

        # Calibration means current velocity should be zero
        self.reset_velocity()

        return list(self.acc_offset_body)

if __name__ == "__main__":
    estimator = IMUVelocityEstimator()

    # Example calibration samples.
    # In real use, collect these while the IMU is static.
    calib_samples = [
        ([0.2, 0.73, 0.66], [46, -10, -8]),
        ([0.201, 0.731, 0.661], [46, -10, -8]),
        ([0.199, 0.729, 0.659], [46, -10, -8]),
    ]

    offset = estimator.calibrate_acc_offset(calib_samples)
    print("acc_offset_body =", offset)

    dt = 0.01

    acc = [0.22, 0.75, 0.67]
    rpy = [46, -10, -8]

    acc_world_linear, vel_world, vel_body = estimator.update(acc, rpy, dt)

    print("acc_world_linear =", acc_world_linear)
    print("vel_world        =", vel_world)
    print("vel_body         =", vel_body)