from machine import Pin, PWM
import time

class Servo:
    def __init__(self, pin_num, max_speed=120.0, max_accel=360.0, min_angle=0.0, max_angle=180.0, enabled=True):
        self.pwm = PWM(Pin(pin_num))
        self.pwm.freq(50)

        # motion state
        self.pos = 90.0
        self.vel = 0.0
        self.target = 90.0

        # tuning
        self.max_speed = float(max_speed)
        self.max_accel = float(max_accel)

        # PERFORMANCE: Precompute inverse of 2*accel to use multiplication instead of division later
        self._inv_2_accel = 1.0 / (2.0 * self.max_accel)

        # limits
        self.min_angle = float(min_angle)
        self.max_angle = float(max_angle)

        # tolerances
        self.pos_tolerance = 0.6
        self.vel_tolerance = 1.0
        
        # PERFORMANCE: Precompute PWM conversion constants to avoid heavy math in the update loop
        # Mapping 0..180 to 500..2500us on a 20000us period, scaled to 65535
        self._pwm_offset = 500.0 * (65535.0 / 20000.0)
        self._pwm_slope = (2000.0 / 180.0) * (65535.0 / 20000.0)

        # debug flag
        self.enabled = enabled

        # Start limp to prevent power spike on startup
        self.pwm.duty_u16(0)

    def set_target(self, angle):
        # Inline clamp to save function call overhead
        angle = float(angle)
        self.target = self.min_angle if angle < self.min_angle else (self.max_angle if angle > self.max_angle else angle)

    def update(self, dt):
        if not self.enabled or dt <= 0:
            return

        error = self.target - self.pos
        dist = abs(error)
        
        desired_dir = 0.0 if dist < 1e-9 else (1.0 if error > 0 else -1.0)
        vel_dir = 0.0 if abs(self.vel) < 1e-9 else (1.0 if self.vel > 0 else -1.0)

        # small-target case
        if dist <= self.pos_tolerance and abs(self.vel) <= self.vel_tolerance:
            self.pos = self.target
            self.vel = 0.0
            self._write_pwm(self.pos)
            return

        # braking-first rule
        if vel_dir != 0 and desired_dir != 0 and vel_dir != desired_dir:
            accel = -vel_dir * self.max_accel
        else:
            # PERFORMANCE: Using precomputed multiplication instead of division
            stopping_dist = (self.vel * self.vel) * self._inv_2_accel 
            if dist > stopping_dist:
                accel = desired_dir * self.max_accel
            else:
                accel = -desired_dir * self.max_accel

        # integrate velocity
        new_vel = self.vel + accel * dt

        # avoid sign flip jitter
        if (self.vel > 0 and new_vel < 0) or (self.vel < 0 and new_vel > 0):
            new_vel = 0.0

        # Inline clamp speed
        self.vel = -self.max_speed if new_vel < -self.max_speed else (self.max_speed if new_vel > self.max_speed else new_vel)

        # integrate position
        self.pos += self.vel * dt

        # Inline clamp position to valid servo range
        self.pos = 0.0 if self.pos < 0.0 else (180.0 if self.pos > 180.0 else self.pos)

        self._write_pwm(self.pos)

    def _write_pwm(self, angle):
        # PERFORMANCE: Single multiplication and addition
        duty = int(self._pwm_offset + self._pwm_slope * angle)
        self.pwm.duty_u16(duty)
