import math
from typing import Optional
from dataclasses import dataclass
import cadquery as Cq
from nhf import Item, Role

@dataclass(frozen=True)
class TorsionSpring(Item):
    """
    A torsion spring with abridged geometry (since sweep is slow)
    """
    # Outer radius
    radius: float = 12.0
    height: float = 20.0
    thickness: float = 2.0

    # Angle (in degrees) between the two legs at neutral position
    angle_neutral: float = 90.0

    tail_length: float = 25.0
    right_handed: bool = False

    torsion_rate: Optional[float] = None

    @property
    def name(self) -> str:
        return f"TorsionSpring-{int(self.radius)}-{int(self.height)}"

    @property
    def radius_inner(self) -> float:
        return self.radius - self.thickness

    def torque_at(self, theta: float) -> float:
        return self.torsion_rate * theta

    def generate(self, deflection: float = 0) -> Cq.Workplane:
        omega = self.angle_neutral + deflection
        omega = -omega if self.right_handed else omega
        base = (
            Cq.Workplane('XY')
            .cylinder(height=self.height, radius=self.radius,
                      centered=(True, True, False))
        )
        base.faces(">Z").tag("top")
        base.faces("<Z").tag("bot")

        tail = Cq.Solid.makeCylinder(
            height=self.tail_length,
            radius=self.thickness / 2)
        # points cylinder to +X
        dy = self.radius - self.thickness / 2
        if self.right_handed:
            dy = -dy
        loc_dir_x = Cq.Location((0, 0, self.thickness / 2), (0, 1, 0), 90)
        loc_shift = Cq.Location((0, dy, 0))
        loc_top = Cq.Location((0, 0, self.height - self.thickness), (0, 0, 1), omega + 180)
        result = (
            base
            .cylinder(
                height=self.height,
                radius=self.radius - self.thickness,
                combine='s',
                centered=(True, True, True))
            .union(tail.located(loc_shift * loc_dir_x))
            .union(tail.located(loc_top * loc_shift.inverse * loc_dir_x))
            .clean()
        )
        r = -self.radius if self.right_handed else self.radius
        plane = result.copyWorkplane(Cq.Workplane('XY'))
        plane.polyline([(0, r, 0), (self.tail_length, r, 0)],
                       forConstruction=True).tag("dir_bot")
        omega = math.radians(omega)
        c, s = math.cos(omega), math.sin(omega)
        l = -self.tail_length
        plane.polyline([
            (-s * r,        c * r, self.height),
            (c * l - s * r, c * r + s * l, self.height)],
                       forConstruction=True).tag("dir_top")
        return result