from enum import StrEnum
import numpy as np


class ParseableEnum(StrEnum):
    """StrEnum with a shared from_any()/from_token() contract.

    Subclasses must implement from_token(). Override _default() to
    control what from_any(None) returns (default: None).
    """

    @classmethod
    def _default(cls):
        return None

    @classmethod
    def from_any(cls, arg):
        if arg is None:
            return cls._default()
        if isinstance(arg, cls):
            return arg
        if isinstance(arg, int):
            members = list(cls)
            if 0 <= arg < len(members):
                return members[arg]
            raise ValueError(
                f"Index {arg} out of range for {cls.__name__} "
                f"(valid: 0–{len(members) - 1})"
            )
        return cls.from_token(arg)

    @classmethod
    def from_token(cls, token):
        raise NotImplementedError


class UnitEnum(ParseableEnum):
    # subclass contract: members are defined as (token, to_base, aliases) and have a default classmethod
    def __new__(cls, token: str, to_base: float, aliases=()):
        obj = str.__new__(cls, token)
        obj._value_ = token
        obj.to_base = float(to_base)
        obj.aliases = tuple(a.lower() for a in aliases)
        return obj

    @classmethod
    def labels(cls) -> list:
        return [m.value for m in cls]

    @classmethod
    def from_token(cls, token):
        t = str(token).strip().lower()
        for u in cls:
            if t == u.value or t in u.aliases:
                return u
        raise ValueError(f"Unknown unit {token!r}. Valid units are {cls.labels()}")

    @classmethod
    def from_any(cls, arg):
        if arg is None:
            return cls.default
        if isinstance(arg, cls):
            return arg
        if isinstance(arg, int):
            members = list(cls)
            if 0 <= arg < len(members):
                return members[arg]
            raise ValueError(
                f"Index {arg} out of range for {cls.__name__} "
                f"(valid: 0–{len(members) - 1})"
            )
        return cls.from_token(arg)


class LengthUnits(UnitEnum):
    METERS = ("meters", 1.0, ("m", "meter"))
    FEET = ("feet", 0.3048, ("ft", "foot"))
    INCHES = ("inches", 0.0254, ("in", "inch"))
    CENTIMETERS = ("centimeters", 0.01, ("cm", "centimeter"))
    MILLIMETERS = ("millimeters", 0.001, ("mm", "millimeter", "milimeter"))
    YARDS = ("yards", 0.9144, ("yard",))

    @classmethod
    def default(cls):
        return cls.METERS


class TimeUnits(UnitEnum):
    SECONDS = ("seconds", 1.0, ("s", "secs", "sec"))
    MINUTES = ("minutes", 60.0, ("min", "mins"))
    HOURS = ("hours", 3600.0, ("hour", "hr"))
    DAYS = ("days", 86400.0, ("d", "days"))

    @classmethod
    def default(cls):
        return cls.SECONDS


def convert(
    enum,
    src,
    dst,
    *args,
    sigfigs=None,
    default_src=None,
    default_dst=None,
):
    s = enum.from_any(src)
    d = enum.from_any(dst)
    if s == d:
        return args[0] if len(args) == 1 else args

    factor = s.to_base / d.to_base
    out = tuple(None if a is None else a * factor for a in args)
    if sigfigs is not None:
        out = tuple(None if a is None else np.round(a, sigfigs) for a in out)
    return out[0] if len(out) == 1 else out


def convert_length(src, dst, *args, sigfigs: int | None = 12):
    return convert(LengthUnits, src, dst, *args, sigfigs=sigfigs)


def convert_time(src, dst, *args, sigfigs: int | None = None):
    return convert(TimeUnits, src, dst, *args, sigfigs=sigfigs)


def convert_units(src, dst, *args, sigfigs: int | None = 12, unit_type="length"):
    if unit_type.lower() == "length":
        return convert_length(src, dst, *args, sigfigs=sigfigs)
    elif unit_type.lower() == "time":
        return convert_time(src, dst, *args, sigfigs=sigfigs)
    raise ValueError(f"{unit_type} is not a valid unit type")
