from abc import ABC,abstractmethod
from sympy import *
from pysatl import Utils
from collections import OrderedDict
from io import BytesIO

class NotInitializedError(RuntimeError):
    pass

class XCType(ABC):

    def get_val(self):
        return self._value 
    
    def set_val(self,value):
        self._value = value

    def get_context(self):
        return self._context 
    
    def set_context(self,value):
        self._context = value

    def __init__(self, init=None):
        self._type_name = None
        self._members = None
        self._value = None
        self._context = None
        if init is not None:
            self._type_name = init._type_name
            self._members = init._members
            self._value = init._value
            self._context = init._context

    def do_init(self,init):
        if init is not None:
            init_bytes = init.conv_to_bytes()
            if len(init_bytes) != self.get_size():
                raise RuntimeError('size mismatch')
            self.conv_from_bytes(init_bytes)

    def __str__(self):
        return self.conv_to_str(deep=False)

    def conv_to_str(self,*,deep=False,indent=''):
        if self._value is not None:
            # primitive type with concrete value
            return f'{indent}{self._value:#0{self.get_size()*2+2}x}'
        if self._members is None:
            # primitive type without concrete value
            return indent+self._type_name
        # struct
        out = f'{indent}{self._type_name} ({self.get_size()} bytes)\n'
        
        #return the value of each member
        indent += '  '
        for name in self._members:
            m = self._members[name]
            if deep:
                m_str = m.conv_to_str(deep=True,indent=indent)
                out += f'{indent}{name}: {m_str}'
            else:
                out += f'{indent}{name}: {m._type_name} ({m.get_size()} bytes)'
                if m.get_is_initialized():
                    if m.get_is_primitive():
                        out += f' = {m}'
                    else:
                        out += f' = {Utils.hexstr(m.conv_to_bytes())}'
            out += '\n'
        
        return out

    def get_size(self):
        # primitive type shall override this
        out = 0
        for m in self._members.values():
            out += m.get_size()
        return out
    
    @staticmethod
    def get_is_primitive():
        return False

    def get_is_initialized(self):
        try: 
            self.conv_to_bytes()
            return True
        except NotInitializedError:
            return False

    def conv_to_bytes(self) -> bytes:
        if self._members is None:
            if self._value is None:
                raise NotInitializedError()
            return self._value.to_bytes(self.get_size(),byteorder='little')
        out = bytearray()
        for m in self._members.values():
            out += m.to_bytes()
        return out
    
    def _check_size(self, data: bytes):
        if len(data) != self.get_size():
            raise RuntimeError(f'size mismatch: {len(data)} vs {self.get_size()}')

    def conv_from_bytes(self, data: bytes):
        if self._members is None:
            self._check_size(data)
            self._value = int.from_bytes(data,byteorder='little')
        else:
            data_bytes = BytesIO(data)
            consumed = 0
            context = self._context
            for name in self._members:
                size = self._members[name].get_size()
                if not isinstance(size,int):
                    expression = size
                    vars = sorted(expression.free_symbols)
                    evaluated = expression.subs(context)
                    size = evaluated
                    self._members[name]._context = context
                consumed += size
                dat = data_bytes.read(size)
                self._members[name].conv_from_bytes(dat)
                if context is not None:
                    context[name] = self._members[name]._value
            self._context = context
            if consumed != len(data):
                raise RuntimeError(f'size mismatch: {len(data)} vs {consumed}')

class BasePrimitive(XCType):   
    def set_val(self,value):
        if value>self._type_mask:
            raise OverflowError()
        self._value = value & self._type_mask

    def __init__(self, init=None, size=None, name=None):
        super().__init__()
        self._type_size = size
        self._type_mask = (1<<(size*8)) - 1
        self._type_name = name
        self._members = None
        if init is not None:
            self.set_val(int(init))

    def get_size(self):
        return self._type_size
    
    @staticmethod
    def get_is_primitive():
        return True
    
class U8(BasePrimitive):
    def __init__(self, init=None):
        super().__init__(init=init,size=1,name='uint8_t')

class U16(BasePrimitive):
    def __init__(self, init=None):
        super().__init__(init=init,size=2,name='uint16_t')

class U32(BasePrimitive):
    def __init__(self, init=None):
        super().__init__(init=init,size=4,name='uint32_t')

class U64(BasePrimitive):
    def __init__(self, init=None):
        super().__init__(init=init,size=8,name='uint64_t')

class U128(BasePrimitive):
    def __init__(self, init=None):
        super().__init__(init=init,size=16,name='uint128_t')

class S8(BasePrimitive):
    def __init__(self, init=None):
        super().__init__(init=init,size=1,name='int8_t')

class S16(BasePrimitive):
    def __init__(self, init=None):
        super().__init__(init=init,size=2,name='int16_t')

class S32(BasePrimitive):
    def __init__(self, init=None):
        super().__init__(init=init,size=4,name='int32_t')

class S64(BasePrimitive):
    def __init__(self, init=None):
        super().__init__(init=init,size=8,name='int64_t')

class S128(BasePrimitive):
    def __init__(self, init=None):
        super().__init__(init=init,size=16,name='int128_t')


class BaseStruct(XCType):
    def __init__(self, init=None):
        super().__init__()
        self._members = OrderedDict()
        self._context = dict()

class Array(XCType):
    def __init__(self, init=None,elem_class=None,num_elem=None):
        super().__init__()
        self._type_name = 'array'
        self._context = dict()
        self._num_elem_int = None
        self._num_elem_sym = None
        if init is not None:
            self.do_init(init)
        else:
            self._elem_class = elem_class 
            if isinstance(num_elem,int):
                self._num_elem_int = num_elem
                self._value = [elem_class() for i in range(num_elem)]
            else:
                self._num_elem_sym = num_elem
            if elem_class is None:
                raise RuntimeError('elem_class is mandatory when init is None')

    def elem_size(self):
        return self._elem_class().get_size() # we support only element types with fixed size

    def get_size(self):
        num_elem = self._num_elem_int
        if num_elem is None:
            num_elem = self._num_elem_sym
            try:
                num_elem = num_elem.subs(self._context)
                self._num_elem_int = int(num_elem)
            except:
                pass
        return num_elem * self.elem_size()

    def conv_to_bytes(self) -> bytes:
        out = bytearray()
        if self._value is None:
            raise NotInitializedError()
        for m in self._value:
            out += m.conv_to_bytes()
        return out
    
    def conv_from_bytes(self, data: bytes):
        size = self.get_size()
        if len(data) != size:
            raise RuntimeError(f'size mismatch: {len(data)} vs {size}')
        data_bytes = BytesIO(data)
        elem_size = self.elem_size()
        self._value = [self._elem_class() for i in range(self._num_elem_int)]
        for e in self._value:
            e.conv_from_bytes(data_bytes.read(elem_size))

    def conv_to_str(self,*,deep=False,indent=''):
        if self.get_is_initialized():
            out = indent+'{'
            sep=''
            for e in self._value:
                out += f'{sep}{e.conv_to_str(deep=deep,indent='')}'
                sep = ', '
            out += '}'
        else:
            num_elem = self._num_elem_int
            if num_elem is None:
                num_elem = self._num_elem_sym
            out = indent+'{'+self._elem_class()._type_name+'}*'+f'{num_elem}'
        return out


from collections.abc import MutableMapping, MutableSequence, MutableSet
import copy

MUTABLES = MutableMapping, MutableSequence, MutableSet  # Mutable containers

def managed_attribute(name):
    """Return a property that stores values under a private non-public name."""
    storage_name = '_' + name.lower()

    @property
    def prop(self):
        return getattr(self, storage_name)

    @prop.setter
    def prop(self, value):
        #setattr(self, storage_name, value) #TODO: verify it works well
        self._members[name.lower()].set_val(value)

    return prop

def make_struct(classname, **options):
    """Return a class with the specified attributes implemented as properties.
    """
    class Class(BaseStruct):
        def __init__(self, init=None):
            """Initialize instance attribute storage name values."""
            super().__init__()
            self._type_name = classname
            for key, value in options.items():
                if isinstance(value, MUTABLES):  # Mutable?
                    value = copy.deepcopy(value)  # Avoid mutable default arg.
                name = key.lower()
                self._members[name] = value
                setattr(self, '_' + name, value)
            self.do_init(init)

    for key, value in options.items():  # Create class' properties.
        setattr(Class, key.lower(), managed_attribute(key))

    Class.__name__ = classname
    return Class


