from dataclasses import dataclass


DEFAULT_FORCE_CALIBRATION_POINTS = [
    {"raw": 1000.0, "force_n": 0.0},
    {"raw": 10000.0, "force_n": 0.377},
    {"raw": 30000.0, "force_n": 1.377},
    {"raw": 62000.0, "force_n": 2.377},
]

DEFAULT_NORMAL_FORCE_CALIBRATION = {
    "enabled": True,
    "method": "piecewise_linear",
    "extrapolate": True,
    "clamp_output_min": 0.0,
    "points": [
        {"fnormal": 1000.0, "force_n": 0.0},
        {"fnormal": 10000.0, "force_n": 0.377},
        {"fnormal": 30000.0, "force_n": 1.377},
        {"fnormal": 62000.0, "force_n": 2.377},
    ],
}

DEFAULT_SHEAR_FORCE_CALIBRATION = {
    "enabled": True,
    "method": "piecewise_linear",
    "extrapolate": True,
    "clamp_output_min": 0.0,
    "points": DEFAULT_FORCE_CALIBRATION_POINTS,
}


def parse_calibration_point(point):
    if isinstance(point, dict):
        raw = point.get("fnormal", point.get("raw", point.get("x")))
        force_n = point.get("force_n", point.get("n", point.get("y")))
        return float(raw), float(force_n)
    if isinstance(point, (list, tuple)) and len(point) >= 2:
        return float(point[0]), float(point[1])
    raise ValueError(f"invalid calibration point: {point!r}")


@dataclass(frozen=True)
class ForceConverter:
    points: tuple
    unit: str = "N"
    enabled: bool = True
    extrapolate: bool = True
    clamp_output_min: float | None = 0.0
    input_mode: str = "magnitude"
    signed: bool = False

    @classmethod
    def from_config(cls, calibration, config_name):
        if calibration is None:
            calibration = DEFAULT_NORMAL_FORCE_CALIBRATION
        if not calibration.get("enabled", True):
            return cls(points=(), unit="raw", enabled=False)

        method = str(calibration.get("method", "piecewise_linear")).lower()
        if method != "piecewise_linear":
            raise ValueError(f"unsupported {config_name} method: {method}")

        points = tuple(sorted(
            parse_calibration_point(point)
            for point in calibration.get("points", [])
        ))
        if len(points) < 2:
            raise ValueError(f"{config_name}.points must contain at least two points")

        return cls(
            points=points,
            enabled=True,
            extrapolate=bool(calibration.get("extrapolate", True)),
            clamp_output_min=calibration.get("clamp_output_min", 0.0),
            input_mode=str(calibration.get("input", "magnitude")).lower(),
            signed=bool(calibration.get("signed", False)),
        )

    def convert(self, raw):
        if not self.enabled:
            return float(raw)

        raw = float(raw)
        points = self.points
        if raw <= points[0][0]:
            segment = (points[0], points[1])
            if not self.extrapolate:
                return self._clamp(points[0][1])
        elif raw >= points[-1][0]:
            segment = (points[-2], points[-1])
            if not self.extrapolate:
                return self._clamp(points[-1][1])
        else:
            segment = None
            for left, right in zip(points, points[1:]):
                if left[0] <= raw <= right[0]:
                    segment = (left, right)
                    break
            if segment is None:
                return self._clamp(points[-1][1])

        (raw0, n0), (raw1, n1) = segment
        if raw1 == raw0:
            value = n0
        else:
            ratio = (raw - raw0) / (raw1 - raw0)
            value = n0 + ratio * (n1 - n0)
        return self._clamp(value)

    def convert_component(self, raw):
        if not self.signed:
            return self.convert(raw)
        raw = float(raw)
        sign = -1.0 if raw < 0.0 else 1.0
        return sign * self.convert(abs(raw))

    def _clamp(self, value):
        if self.clamp_output_min is not None:
            value = max(float(self.clamp_output_min), value)
        return value
