# AUTOGENERATED! DO NOT EDIT! File to edit: 01_conversion.ipynb (unless otherwise specified).

__all__ = ['data', 'trycast', 'nonitr2itr', 'itr2nonitr', 'itr2itr', 'Caster', 'isiter', 'cast', 'typestr', 'pad',
           'fill', 'nbits', 'num2ar', 'ar2num', 'ar2hex', 'hex2ar', 'str2ar', 'ar2str', 'COPY', 'NOT', 'AND', 'OR',
           'Exclusive_OR', 'XOR', 'ar2gr', 'gr2ar', 'num2gr', 'convert', 'rint', 'RNG']

# Cell
import typing
import numpy as np
from typing import Optional, Tuple, Dict, Callable, Union, Mapping, Sequence, Iterable
from functools import partial
import warnings

# Cell
data = Union[None,int,float,list,tuple,str,dict,set,np.ndarray]

# Cell
def trycast(obj : data, to : type) -> data:
    '''
    Attempts to typecast `obj` to datatype `to`
    using default type conversion. Fallback
    of more complex casting cases in this module.
    '''
    try:
        return to(obj)
    except:
        return typing.cast(to,obj)

# Cell
def nonitr2itr(obj : data, to : type) -> data:
    '''
    Wrap the noniterable `obj` into the iterable type `to`.
    '''
    if to is list:
        return [obj]
    elif to is tuple:
        return (obj,)
    elif to is str:
        return f'{obj}'
    elif to is dict:
        return {obj:obj}
    elif to is set:
        return {obj}
    elif to is np.ndarray:
        return np.array(obj)
    else:
        return trycast(obj,to)

# Cell
def itr2nonitr(obj : data, to : type) -> data:
    '''
    Treat each element of the iterable `obj` as the
    noniterable type `to`.
    '''
    t=type(obj)
    if t is dict:
        return {k:trycast(v,to) for k,v in obj.items()}
    else:
        return trycast([trycast(i,to) for i in obj],to)

# Cell
def itr2itr(obj : data, to : type) -> data:
    '''
    Treat the iterable `obj` as a new iterable of type `to`.
    Treats dictionaries as their items rather than default keys,
    and treats np.ndarray as the callable np.array(obj).
    '''
    if type(obj) is dict:
        return trycast(obj.items(),to)
    elif to is dict:
        return {i:o for i,o in enumerate(obj)}
    elif to is np.ndarray:
        return np.array(obj)
    else:
        return trycast(obj,to)

# Cell
isiter = lambda t: hasattr(t,'__iter__')

class Caster:
    '''
    Universal typecasting class with customizable ruleset.
    Stores the ruleset as a dictionary of dictionaries.
    The outer dictionary stores the type of the object to be converted.
    The inner dictionary stores all the types to convert into.
    The values are partially evaluated functions on the inner type,
    which get called by a class object and evaluated on the outer type.
    The ruleset can be updated and changed as needed.
    '''
    types=[None,int,float,list,tuple,str,dict,set,np.ndarray]

    iterables=[t for t in types if isiter(t)]

    iterables.remove(str) #want to wrap strings like numbers

    def get_rules(types=types,
                  iterables=iterables,
                  itr2itr=itr2itr,
                  nonitr2itr=nonitr2itr,
                  itr2nonitr=itr2nonitr):

        rules={t1:{t2:None for t2 in types} for t1 in types}
        noniterables=[t for t in types if t not in iterables]
        Caster.noniterables=noniterables
        for t1 in types:
            for t2 in types:
                if t1 in noniterables and t2 in iterables:
                    rules[t1][t2]=partial(nonitr2itr,to=t2)
                elif t1 in iterables and t2 in noniterables:
                    rules[t1][t2]=partial(itr2nonitr,to=t2)
                elif t1 in iterables and t2 in iterables:
                    rules[t1][t2]=partial(itr2itr,to=t2)
                else:
                    rules[t1][t2]=partial(trycast,to=t2)

        return rules


    def __init__(self,
                 rules : Optional[dict] = None):
        if rules is None:
            rules=Caster.get_rules()
        self.rules=rules

    def __getitem__(self,item):
        return self.rules[item]


    def __call__(self,
                 obj : data,
                 *args : type):
        res=obj
        for arg in args:
            t=type(res)
            try:
                res=self.rules[t][arg](res)
            except:
                res=trycast(res,arg)
        return res

# Cell
cast=Caster()

# Cell
def typestr(x : data):
    '''
    Parses the string of the input type for readability.
    '''
    if type(x) is type: #if passing type itself
        s=x
    else: #otherwise get type of obj
        s=type(x)
    return str(s).split('<')[-1].split('>')[0].split('class')[-1].split('\'')[1]

# Cell
def pad(data : Union[np.ndarray,list],
          bits : Optional[int] = None,
          to : Union[int,float] = int) -> np.ndarray:
    '''
    Pads an array with zeros, up to a length of `bits`.
    '''
    if bits is None:
        bits=0
    else:
        bits=bits-len(data)
    x=[0 for i in range(bits)]+list(data)
    x=np.array(x).astype(to)
    return x

# Cell
def fill(x : list,fillwith=np.NaN,mask=True):
    '''
    Turn uneven nested lists `x` into arrays `y` substituting
    missing entries using `fillwith` and optionally masking.
    '''
    length = max(map(len, x))
    y=np.array([xi+[fillwith]*(length-len(xi)) for xi in x])
    if mask:
        if np.isfinite(fillwith):
            y=np.ma.masked_equal(y,fillwith)
        else:
            y=np.ma.masked_invalid(y)
    return y

# Cell
def nbits(x : Union[int,float,list,np.ndarray]) -> int:
    '''
    Return the number of bits required to represent the input `x`.
    '''
    t=type(x)
    if (t is int) or (t is float):
        bits=np.ceil(np.log2(x+1)).astype(int) if x!=0 else 1
    elif (t is list) or (t is np.ndarray):
        bits=len(x)
    return bits

# Cell
def num2ar(x: Union[int,float],
            bits : Optional[int] = None,
            to : Union[int,float] = int) -> np.ndarray:
    '''
    Converts decimal number `x` to array `a` zero-padded with `bits`.
    '''
    bits=bits or nbits(x) #if None, give default
    form='0'+str(bits)+'b'
    binary=format(int(x),form)
    with warnings.catch_warnings(): #ignore numpy deprecation warning
        warnings.simplefilter("ignore")
        a=np.fromstring(binary,'u1')-ord('0')
    return cast(a,to)

# Cell
def ar2num(a : Union[list,np.ndarray],
            to : Union[int,float] = int):
    '''
    Converts array `a` to decimal number `x`.
    '''
    temp=str()
    a=np.array(a).astype(int)
    for c in a:
        temp+=str(c)
    x=int(temp,2)
    return cast(x,to)

# Cell
def ar2hex(a : Union[list,np.ndarray],
            bits : Optional[int] = None,
            prefix : bool = True) -> str:
    '''
    Converts binary array to hex string
    in:
        a (numpy array) : binary array to convert
    out:
        h (str) : hex conversion of a
    '''
    bits=bits or nbits(a)
    form='0'+str(int(np.log2(bits)))+'x'
    h=format(ar2num(a),form)
    if prefix:
        h='0x'+h
    return h

# Cell
def hex2ar(h : str,
            bits : Optional[int] = None,
            to : Union[int,float] = int) -> np.ndarray:
    '''
    Converts a hex string `h` into an array `a` padded with `bits` and elements of `astype`.
    '''
    x=int(h,16)
    a=num2ar(x,bits,to)
    return a

# Cell
def str2ar(s : str,
            to : Union[list,np.ndarray] = np.ndarray) -> Union[list,np.ndarray]:
    '''
    Converts an input string `s` into an array or list `a` as per `astype`.
    '''
    a=[int(i) for i in s]
    return cast(a,to)

# Cell
def ar2str(a : Union[list,np.ndarray], to : data = None) -> str:
    '''
    Converts an input array `a` into a string `s`.
    '''
    s=''.join([str(cast(i,to)) for i in a])
    return s

# Cell
def COPY(x : Union[int,float]) -> Union[int,float]:
    '''
    Simply returns `x`.
    '''
    return x

def NOT(x : Union[int,float]) -> Union[int,float]:
    '''
    Return conjugate of `x`.
    '''
    return 1-x

def AND(x : Union[int,float],
        y : Union[int,float]) -> Union[int,float]:
    '''
    Return logical AND of `x` and `y`.
    '''
    return x*y

def OR(x : Union[int,float],
       y : Union[int,float]) -> Union[int,float]:
    '''
    Return logical OR of `x` and `y`. See DeMorgan's Laws.
    '''
    return x+y-x*y

def Exclusive_OR(x : Union[int,float],
                 y : Union[int,float]) -> Union[int,float]:
    '''
    Return logical exclusive OR of `x` and `y`. See DeMorgan's Laws.
    '''
    return OR( AND( x , NOT(y) ) , AND ( NOT(x) , y) )

def XOR(*args : Union[int,float,list,np.ndarray]) -> Union[int,float]:
    '''
    Arbitrary input XOR using recursiveness.
    '''
    x=0
    for a in args:
        x=Exclusive_OR(x,a)
    return x

# Cell
def ar2gr(binary : Union[list,np.ndarray],
             to : Union[list,np.ndarray] = np.ndarray) -> Union[list,np.ndarray]:
    '''
    Converts an input binary array to graycode.
    '''
    binary = cast(binary,int)
    gray = []
    gray += [binary[0]]
    for i in range(1,len(binary)):
        gray += [XOR(binary[i - 1], binary[i])]
    return cast(cast(gray,int),to)

# Cell
def gr2ar(gray : Union[list,np.ndarray],
             to : Union[list,np.ndarray] = np.ndarray) -> Union[list,np.ndarray]:
    '''
    Converts a gray-code array into binary.
    '''
    binary = []
    binary += [gray[0]]
    for i in range(1, len(gray)):
        if (gray[i] == 0):
            binary += [binary[i - 1]]
        else:
            binary += [NOT(binary[i - 1])]
    return cast(cast(binary,int),to)


# Cell
def num2gr(x:int,to:data=None):
    '''
    Converts decimal number `x` to equivalent gray-code number.
    '''
    return int(ar2str(ar2gr(num2ar(x))),2)

# Cell
def convert(obj : Union[int,float,list,hex,str,np.ndarray],
            to : Union[int,float,list,hex,str,np.ndarray] = np.ndarray,
            bits : Optional[int] = None,
            astype : Union[int,float,list,np.ndarray] = int,
            gray : bool = False):
    '''
    Converts an input `obj` into an output of type `to`, padding with `bits`.
    Internally converts `obj` to np.ndarray with elements of dtype `astype`,
    before converting to the desired dtype `to`. If `gray`, first converts this
    binary array to gray-code. If input or output are `hex`, requires prefix of `0x`.

    Possible conversions:
        int -> float
        int -> str
        int -> list
        int -> array
        int -> hex

        str -> int
        str -> float
        str -> list
        str -> array
        str -> hex

        list -> arr
        list -> int
        list -> float
        list -> str
        list -> hex

        arr -> list
        arr -> int
        arr -> float
        arr -> str
        arr -> hex

        hex -> int
        hex -> float
        hex -> arr
        hex -> list
        hex -> str

    '''

    t=type(obj)
    #first convert to binary numpy array
    if (t is np.ndarray) or (t is list):
        x=obj
    elif (t is int) or (t is float):
        x=num2ar(obj,bits,astype)
    else:# t is str
        if obj[:2]=='0x': #obj is hex
            x=hex2ar(obj[2:],bits,astype)
        else:
            x=str2ar(obj)
    x=cast(pad(x,bits),astype)
    g=cast(ar2gr(x),astype)
    #convert
    if (to is np.ndarray):
        if gray:
            return g
        else:
            return x
    elif (to is list) or (to is set):
        if gray:
            return to(g)
        else:
            return to(x)
    elif (to is int) or (to is float) or (to is hex):
        if gray:
            return to(ar2num(g))
        else:
            return to(ar2num(x))
    else:# to is str
        if gray:
            return ar2str(g)
        else:
            return ar2str(x)

# Cell
def rint(x: Union[int,float,list,np.ndarray]) -> Union[int,np.ndarray]:
    '''
    Typecast rounding to np arrays.

    '''
    t=type(x)
    if (t is list) or (t is np.ndarray):
        return np.rint(x).astype(int)
    else:
        return round(x)

# Cell
class RNG:
    '''
    Globally stable random number generator. Initialized with fixed `seed`.
    Contains `normal` and `random` methods, each with an `absval` and `asint` argument,
    which convert to positive values and round to integers respectively.
    '''
    def __init__(self,seed : Optional[int] = 0):
        self.rng=np.random.default_rng(seed)

    def typecast(self,
                 y : Union[int,float],
                 absval : bool = False,
                 asint : bool = False) -> Union[int,float,np.ndarray]:
        if absval:
            y=abs(y)
        if asint:
            y=rint(y)
        return y

    def normal(self,
               x : Union[float,int] = 0,
               y : Union[float,int] = 0,
               shape : Optional[tuple] = None,
               absval : bool = False,
               asint : bool = False) -> Union[int,float,np.ndarray]:
        '''
        Draw from a Gaussian distribution with mean `x` and standard deviation `y`.
        If `shape` is not None, return a numpy array of draws.
        '''
        return self.typecast(y=self.rng.normal(loc=x,scale=y,size=shape),absval=absval,asint=asint)

    def random(self,
               x : Union[float,int] = 0,
               y : Union[float,int] = 0,
               shape : Optional[tuple] = None,
               absval : bool = False,
               asint : bool = False) -> Union[int,float,np.ndarray]:
        '''
        Draw from a uniform distribution in the interval [`x`,`y`].
        If `shape` is not None, return a numpy array of draws.
        '''
        return self.typecast((y-x)*self.rng.random(size=shape),absval=absval,asint=asint)