# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_fact.ipynb.

# %% auto 0
__all__ = ['SI_FACTORS', 'fmeta', 'fact', 'zfact', 'pfact', 'nfact', 'one', 'deca', 'hecto', 'kilo', 'mega', 'giga', 'tera',
           'peta', 'exa', 'zetta', 'yotta', 'ronna', 'quetta', 'deci', 'centi', 'milli', 'micro', 'nano', 'pico',
           'femto', 'atto', 'zepto', 'yocto', 'ronto', 'quecto']

# %% ../nbs/04_fact.ipynb 3
from typing import Any, Optional, ClassVar
from numbers import Number

from sint import sint

# %% ../nbs/04_fact.ipynb 4
from .errs import FactorError
from .cons import BASE, ExpoChr
from .util import getmval, dcsrct

# %% ../nbs/04_fact.ipynb 6
class fmeta(type):
    '''Metaclass for SI unit factors.

    `fmeta` acts as a metaclass for SI unit factors. It sets class variables based on
    the superclass and allows each subclass to have its own definitions. It also handles
    the registration of each factor class.

    Class Attributes
    ----------------
    name : ClassVar[str]
        The name of the factor.
    abrv : ClassVar[str]
        The abbreviation of the factor.
    symb : ClassVar[str]
        The symbol of the factor.
    expo : ClassVar[int]
        The exponent of the factor.
    sign : ClassVar[sint]
        The sign of the exponent (Positive, Negative, or Zero).
    ekey : ClassVar[int]
        Calculated as sign * expo.

    Methods
    -------
    clsdef(cls, attr: str, default: Any = None, **kwargs)
        Get the `attr` from `kwargs`, the class, or the `default`, whichever is found first.
    '''  
    def __new__(cls, name, bases, cls_dict):              
        attrs = ('sign', 'expo')
        vdict = dict((attr, getmval(bases, cls_dict, attr)) for attr in attrs)
        sign, expo = dcsrct(vdict, 'sign', 'expo')        
        cls_dict['ekey'] = sign * expo if not any((sign is None, expo is None)) else None
        
        fcls = super().__new__(cls, name, bases, cls_dict)
        return fcls

# %% ../nbs/04_fact.ipynb 8
class fact(float, metaclass=fmeta):
    '''Class for representing SI unit factors.

    `fact` is a class for representing SI unit factors like kilo, mega, giga, etc.
    It is defined by its name, abbreviation, symbol, and exponent. It provides methods
    to convert values between factors, list all existing factors, and more.

    Class Attributes
    ----------------
    name : ClassVar[str]
        The name of the factor.
    abrv : ClassVar[str]
        The abbreviation of the factor.
    symb : ClassVar[str]
        The symbol of the factor.
    expo : ClassVar[int]
        The exponent of the factor.
    sign : ClassVar[sint]
        The sign of the exponent (Positive, Negative, or Zero).
    ekey : ClassVar[int]
        Calculated as sign * expo.

    Instance Attributes
    -------------------
    base : Optional[int]
        The base for the exponent, defaults to 10.
    echr : ExpoChr
        Character used for indicating exponentiation.
    showsign : bool
        Whether to show the sign in the representation.
    showbase : bool
        Whether to show the base in the representation.
    showefmt : bool
        Whether to show the exponent in the representation.

    Methods
    -------
    clsdef(attr: str, default: Any = None, **kwargs)
        Get the `attr` from `kwargs`, the class, or the `default`, whichever is found first.
    
    iseq(v)
        Checks if the factor is equivalent to the value `v`.

    get(v)
        Retrieves the factor corresponding to the value `v`.

    bump(fcls: 'fact', bump: int = 0)
        Bumps the exponent of the factor by `bump`.

    list(attr: str = 'abrv')
        Lists all registered factors based on the attribute `attr`.

    convert(val: Number, to: 'fact')
        Converts a value to a different factor.

    register(f: 'fact')
        Registers a factor class.

    ispos()
        Checks if the factor is positive.
    isneg()
        Checks if the factor is negative.

    Properties
    ----------
    fstr
        Returns the exponential format of the factor, e.g., F3N.

    bstr
        Returns the base format of the factor, e.g., `10^-3`.

    efmt
        Returns the exponential format of the factor, e.g., 10-3.

    ispos()
        Checks if the factor is positive.

    isneg()
        Checks if the factor is negative.
    '''
    # ------------------------------------------------------------------------------------
    # class variables & methods
    # ------------------------------------------------------------------------------------
    name: ClassVar[str]
    abrv: ClassVar[str]
    symb: ClassVar[str]
    expo: ClassVar[int] = 0
    sign: ClassVar[sint] = None
    ekey: ClassVar[int] # NOTE: sign * expo, defined in `fmeta`

    @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))
        if attr == 'echr': val = ExpoChr.to(val)
        if attr == 'ekey': val = cls.sign * cls.expo
        return default if val is None else val
    
    def __new__(cls, base: Optional[int] = BASE, **kwargs) -> 'fact':
        '''Set the float to be the powered value of the base and the exponent'''
        numb = sint.topow(base, cls.expo, cls.sign)
        obj = float.__new__(cls, numb)
        obj.base = base        
        return obj

    def __init__(self, *args, **kwargs) -> None:
        for attr in ('echr', 'showsign', 'showbase', 'showefmt'):
            setattr(self, attr, self.clsdef(attr, **kwargs))

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.register(cls)   

    @classmethod
    def iseq(cls, v) -> bool:
        '''Check if the class or its attributes match a given value.

        Parameters
        ----------
        v : Any
            The value to be checked.

        Returns
        -------
        bool
            True if the value matches the class or any of its attributes (`name`, `abrv`, `symb`, `ekey`), otherwise False.
        '''       
        if cls == v: return True
        keys = ('name', 'abrv', 'symb', 'ekey',)
        if any(v == getattr(cls, attr, None) for attr in keys): return True
        return False

    @classmethod
    def get(cls, v) -> Optional['fact']:
        '''Retrieve a factor class that matches a given value.

        Parameters
        ----------
        v : Any
            The value to be checked.

        Returns
        -------
        Optional['fact']
            Returns the factor class if found, otherwise None.
        '''  
        for f in cls.facts:
            if f.iseq(v): return f
        return None
    
    @classmethod
    def bump(cls, fcls: 'fact', bump: int = 0) -> 'fact':
        '''Bump the exponent of the factor by a given amount.

        Parameters
        ----------
        fcls : 'fact'
            The factor class to bump.
        bump : int, optional
            The amount to bump the exponent by, defaults to 0.

        Returns
        -------
        'fact'
            The new factor class with the bumped exponent.

        Examples
        --------
        >>> tuple((i, fact.moveunit(one, i).name) for i in range(-13, 14, 1))
        '''
        exps = sorted({f.expo for f in cls.facts}) # [0, 1, 2, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30]
        sign, expo = fcls.sign if fcls.sign != sint.Z else 1, fcls.expo
        
        # Get current index of the exponent
        eidx = exps.index(expo)
        
        nidx = ((sign * eidx) + bump)
        if nidx < 0: sign, nidx = -sign, abs(nidx) # Adjust negative indices

        # Handle sign change when crossing boundaries e.g. `<= -30` or `>= +30`
        if len(exps) <= abs(nidx): return fact.get(sign * exps[len(exps) - 1]) 
        return cls.get(sign * exps[nidx]) # Get the new exponent
    
    @classmethod
    def list(cls, attr: str = 'abrv') -> list:
        '''List all registered factors based on a given attribute.

        Parameters
        ----------
        attr : str, optional
            The attribute to list, defaults to 'abrv'.

        Returns
        -------
        list
            A list of all registered factors based on the attribute.
        '''
        return [getattr(f, attr, None) for f in cls.facts]
        
    @classmethod
    def convert(cls, val: Number, to: 'fact') -> float:
        '''Convert a number to a different factor.

        Parameters
        ----------
        val : Number
            The value to convert.
        to : 'fact'
            The factor class to convert to.

        Returns
        -------
        float
            The converted value.

        Raises
        ------
        FactorError
            If the target factor is not found.
        '''
        fcls = cls.get(to)
        if not fcls: raise FactorError(to)
        return val * float(fcls(getattr(val, 'base', cls.base)))
    
    # ------------------------------------------------------------------------------------
    # class / subclass shared variables & methods
    # ------------------------------------------------------------------------------------
    facts: ClassVar[set['fact']] = set()
    @classmethod
    def register(cls, f: 'fact'):
        rattrs = ('name', 'expo', 'sign') # required attributes to be a valid `fact` subclass
        hasall = all(hasattr(f, attr) for attr in rattrs)
        if not hasall and not isinstance(f, cls): return
        cls.facts.add(f)

    # ------------------------------------------------------------------------------------
    # instant variables & methods
    # ------------------------------------------------------------------------------------
    base: Optional[int] = BASE
    echr: ExpoChr  = ExpoChr.carot
    showsign: bool = True
    showbase: bool = False    
    showefmt: bool = True

    @property
    def fstr(self):
        '''Returns the exponential format of the factor e.g. F3N'''
        return sint.make_fstr(self.expo, self.sign, self.base, self.echr, self.showsign, self.showbase)        
    @property
    def bstr(self):
        '''Returns the base format of the factor e.g. `10^-3`'''
        return sint.make_bstr(self.expo, self.sign, self.base, self.echr, self.showsign, self.showbase)        
    @property
    def efmt(self):
        '''Returns the exponential format of the factor e.g. 10-3'''
        return sint.make_estr(self.expo, self.sign, self.base, self.echr, self.showsign, self.showbase)    

    def ispos(self): return sint.to(self.sign) == sint.P
    def isneg(self): return sint.to(self.sign) == sint.N
    def __str__(self) -> str:  return f'{self.efmt}' if self.showefmt else f'{self.fstr}'
    def __repr__(self) -> str: return str(self)

# %% ../nbs/04_fact.ipynb 10
# fact subclasses for positive (`pfact`, e.g. e+<n>), negative (`nfact`, e.g. e-<n>), and the "base" factor (`zfact` e.g. e0)
class zfact(fact): abrv, symb, expo, sign =  None, None, 0, sint.Z
class pfact(fact): sign = sint.P
class nfact(fact): sign = sint.N

# %% ../nbs/04_fact.ipynb 13
# zfact subclass for the "base" factor (e0)
class one(zfact): name = 'one'

# %% ../nbs/04_fact.ipynb 15
# pfact subclasses
class deca(pfact): name, abrv, symb, expo = 'ten', 'deca', 'da', 1
class hecto(pfact): name, abrv, symb, expo = 'hundred', 'hecto', 'h', 2
class kilo(pfact): name, abrv, symb, expo = 'thousand', 'kilo', 'k', 3
class mega(pfact): name, abrv, symb, expo = 'million', 'mega', 'M', 6
class giga(pfact): name, abrv, symb, expo = 'billion', 'giga', 'G', 9
class tera(pfact): name, abrv, symb, expo = 'trillion', 'tera', 'T', 12
class peta(pfact): name, abrv, symb, expo = 'quadrillion', 'peta', 'P', 15
class exa(pfact): name, abrv, symb, expo = 'quintillion', 'exa', 'E', 18
class zetta(pfact): name, abrv, symb, expo = 'sextillion', 'zetta', 'Z', 21
class yotta(pfact): name, abrv, symb, expo = 'septillion', 'yotta', 'Y', 24
class ronna(pfact): name, abrv, symb, expo = 'octillion', 'ronna', 'R', 27
class quetta(pfact): name, abrv, symb, expo = 'nonillion', 'quetta', 'Q', 30

# %% ../nbs/04_fact.ipynb 17
# nfact subclasses
class deci(nfact): name, abrv, symb, expo = 'tenth', 'deci', 'd', 1
class centi(nfact): name, abrv, symb, expo = 'hundredth', 'centi', 'c', 2
class milli(nfact): name, abrv, symb, expo = 'thousandth', 'milli', 'm', 3
class micro(nfact): name, abrv, symb, expo = 'millionth', 'micro', 'u', 6
class nano(nfact): name, abrv, symb, expo = 'billionth', 'nano', 'n', 9
class pico(nfact): name, abrv, symb, expo = 'trillionth', 'pico', 'p', 12
class femto(nfact): name, abrv, symb, expo = 'quadrillionth', 'femto', 'f', 15
class atto(nfact): name, abrv, symb, expo = 'quintillionth', 'atto', 'a', 18
class zepto(nfact): name, abrv, symb, expo = 'sextillionth', 'zepto', 'z', 21
class yocto(nfact): name, abrv, symb, expo = 'septillionth', 'yocto', 'y', 24
class ronto(nfact): name, abrv, symb, expo = 'octillionth', 'ronto', 'r', 27
class quecto(nfact): name, abrv, symb, expo = 'nonillionth', 'quecto', 'q', 30

# %% ../nbs/04_fact.ipynb 19
SI_FACTORS = (    
    one, 
    deca, hecto, kilo, mega, giga, tera, peta, exa, zetta, yotta, ronna, quetta, 
    deci, centi, milli, micro, nano, pico, femto, atto, zepto, yocto, ronto, quecto
)
for f in SI_FACTORS: f.register(f)
fact.facts = set(SI_FACTORS)
