# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_unit.ipynb.

# %% auto 0
__all__ = ['umeta', 'unit']

# %% ../nbs/05_unit.ipynb 3
from typing import Any, Optional, ClassVar
from pstr.fmts import fmtunit

# %% ../nbs/05_unit.ipynb 4
from .atyp import FactRepr
from .errs import FactorError
from .cons import BASE
from .fact import fact, one

# %% ../nbs/05_unit.ipynb 6
class umeta(type):
    '''A metaclass for setting SI unit properties on the unit class.

    The `umeta` metaclass attaches SI unit properties to classes at the time of their creation. 
    It pulls the attributes from the `fact` class and sets them as properties on the class it 
    initializes.

    Attributes
    ----------
    units : set['fact']
        Set of SI unit factors, populated from `fact.facts`.

    Methods
    -------
    __new__(cls, name, bases, cls_dict)
        Create a new instance of the metaclass, setting the unit attributes.

    Examples
    --------
    >>> class unit(float, metaclass=umeta):
    ...     pass
    >>> unit.units
    (one, decka, hecto, ..., quetta, deci, ..., quecto)
    '''
    
    def __new__(cls, name, bases, cls_dict):
        # Generate the attributes for the SI units
        cls_dict['units'] = fact.facts # e.g. (one, decka, hecto, ..., quetta, deci, ..., quecto)
        ucls = super().__new__(cls, name, bases, cls_dict)
        
        for fcls in fact.facts:            
            cfunc = ucls.getcfunc(fcls=fcls)
            for attr in ('abrv', 'name', 'symb'):
                if (aval := getattr(fcls, attr, None)) is not None: 
                    setattr(ucls, aval, property(cfunc))        
        return ucls

# %% ../nbs/05_unit.ipynb 8
class unit(float, metaclass=umeta):
    '''Class for representing SI units, inheriting from float and using `umeta` as the metaclass.

    The `unit` class defines the properties and methods for handling units in the SI system. 
    It allows for conversion between different units and representations.

    Class Attributes
    ----------------
    name : ClassVar[str]
        The name of the unit.
    debug : ClassVar[bool]
        Debugging flag to control representation.

    Instance Attributes
    -------------------
    core : Optional[float]
        The actual float value stored.
    base : Optional[int]
        The base for the exponent, defaults to 10.
    fcls : Optional[fact]
        The factor class being used, defaults to `one`.
    numb : Optional[float]
        The number used for conversion.
    ndig : Optional[int]
        Number of digits to display.
    shownumb : Optional[bool]
        Whether to show the number in representation.
    abrvunit : Optional[bool]
        Whether to abbreviate the unit.
    factrepr : Optional[FactRepr]
        How to represent the factor ('abrv', 'name', 'symb').

    Methods
    -------
    getcfunc(fcls: fact)
        Returns a class method for converting the unit to the given factor class.
    factor()
        Actually applies the factor to the base.
    to(key, inplace: bool = False)
        Converts the unit to a different factor.
    '''
    # ------------------------------------------------------------------------------------
    # class variables & methods
    # ------------------------------------------------------------------------------------
    name: ClassVar[str]
    debug: ClassVar[bool] = False
    @classmethod
    def getcfunc(cls: type, fcls: fact):
        '''returns a class method that converts the unit to the given factor class'''
        def cfunc(self, inplace: bool = False):
            return self.to(fcls, inplace)
        return cfunc

    # ------------------------------------------------------------------------------------
    # instant variables & methods
    # ------------------------------------------------------------------------------------
    core: Optional[float]        # NOTE: this is the number that is actually stored in the float    
    base: Optional[int] = BASE   # base to use for the conversion
    fcls: Optional[fact] = one   # factor class to use, defaults to `one` (e.g. e0=1) so that `unit` works as float as expected by default
    numb: Optional[float] = None # NOTE: this is the number used for the conversion
    ndig: Optional[int] = 3      # number of digits to show

    shownumb: Optional[bool] = True
    abrvunit: Optional[bool] = True
    factrepr: Optional[FactRepr] = 'symb'

    def __new__(cls, core: float, *args, **kwargs):
        obj = float.__new__(cls, core)
        return obj
    
    @classmethod
    def clsdef(cls, attr: str, default: Any = None, **kwargs):
        '''Get the `attr` from `kwargs`, the class, or the `default`, whichever is found first'''
        val = kwargs.get(attr, getattr(cls, attr, default))
        return default if val is None else val

    def __init__(self, core: float, **kwargs):
        attrs = ('numb', 'base', 'fcls', 'ndig', 'shownumb', 'abrvunit', 'factrepr')
        adefs = tuple(self.clsdef(attr, (core if attr == 'numb' else None), **kwargs) for attr in attrs)
        for i, attr in enumerate(attrs):
            setattr(self, attr, adefs[i])


    @property
    def factor(self) -> fact:
        '''actually applies the factor to the base'''
        return self.fcls(self.base)

    def __str__(self) -> str:
        ustr = fmtunit(
            org = float(self.numb), flt = float(self), unt = self, fct = self.fcls,
            factrepr = self.factrepr, shownumb = self.shownumb,
            abrvunit = self.abrvunit, unitname = self.name, ndig = self.ndig
        )
        return ustr
    
    def __repr__(self) -> str:
        if type(self).debug:
            return f'{float(self)} (o={self.numb}, b={self.base}, f={self.fcls.__name__})'        
        return str(self)

    def to(self, key, inplace: bool = False):
        '''Converts the unit to a different factor.

        This method changes the unit of the number to the unit specified by the `key`.
        The conversion is either done in-place or a new object is returned based on the
        `inplace` parameter.

        Parameters
        ----------
        key : Any
            The key used to look up the factor class. It could be an abbreviation, name, 
            symbol, or the factor class itself.
        inplace : bool, optional
            If True, the conversion is done in-place and the object itself is modified.
            Otherwise, a new object with the converted value is returned. Default is False.

        Returns
        -------
        unit
            A new `unit` object with the converted value if `inplace` is False, otherwise 
            modifies the object itself and returns it.

        Raises
        ------
        FactorError
            If the provided `key` is not found in the available factors.

        Examples
        --------
        >>> u = unit(1000, base=10, fcls=kilo)
        >>> u.to(mega)
        unit(1)
        >>> u.to(mega, inplace=True)
        unit(1)
        '''
        fcls = fact.get(key)
        if not fcls: raise FactorError(key)
        fcls = fcls(self.base)
        curr = self.numb / fcls
        if inplace:
            return type(self)(core=curr, numb=curr, base=self.base, fcls=one(self.base))        
        return type(self)(core=curr, numb=self.numb, base=self.base, fcls=fcls)
    
