import math
import json
from pymoku import log
from pymoku.registers import CachableAccessControl, RegisterAccess, RegisterAccessControl
from pymoku.network import CacheableRenderControl
from pymoku.sources import Input, Output


class _RegInput(RegisterAccess, Input):
    def __init__(self, parent, name, regs):
        RegisterAccess.__init__(self, parent, 0, name)
        bus_width = len(regs)
        self.inp_regs = self.reg_props(regs)
        self.reg_length = regs[0].length
        Input.__init__(self, name, parent, bus_width)

    def set_idx(self, idx):
        if idx is None:
            idx = [2**self.reg_length - 1] * self.bus_width
        self.inp_regs[:] = idx

    def get_idx(self):
        if self.inp_regs[0] == 2**self.reg_length - 1:
            return None
        return tuple(self.inp_regs[:])


class Slot(CachableAccessControl):
    ID = 'pymoku:empty'
    NAME = "Empty"
    APPLET = None

    def __init__(self, moku, slot=0):
        super().__init__(f'raw{slot:d}', moku._control)
        self._arb_data = RegisterAccessControl(f'arb{slot:d}', moku._control)

        self.slot = slot
        self.moku = moku
        self.attached = False

        self.__inputs = {}
        self.__outputs = {}

        deploy_state = self.moku.modify_hardware()[f'instr{slot:d}']
        config = deploy_state['config']
        self.num_ain = config.get('num_ain', config.get('ains'))
        self.num_aout = config.get('num_aout', config.get('aouts'))
        self.num_cbufs = config.get('num_cbufs', config.get('cbufss'))
        self.timestamp = deploy_state['timestamp']

        self.rdr_ctrl = CacheableRenderControl(moku.socket_factory)
        log.debug(f"created {deploy_state['ID']} with config {config}")
        self.init_regs(**config)

    def init_regs(self, **kwargs):
        """ Add default regs
            should be overridden
        """
        pass

    def hardware_modified(self, params, state):
        """ Callback when a Moku's modify_hardware completes
            'params' is a dictionary of the request
            'state' is the complete state after the request
        """
        pass

    def cbufs(self):
        """Provide the list of cbufs attached to this slot given
           it's idx"""
        return self.moku.cbufs()[self.slot]

    def reset(self):
        pass

    def attach(self):
        self.attached = True

    def dettach(self):
        self.attached = False

    def sys_ref(self):
        return self.moku.sys_ref()

    def add_input(self, inp):
        self.__inputs[inp.name] = inp
        return inp

    def add_reg_input(self, name, regs):
        """ Shortcut for mapping an input to a set of registers

        regs is a list of register accessors that will be resolved from 'self''
        """
        return self.add_input(_RegInput(self, name, regs))

    def input(self, name):
        """ Get an Input for this slot

        Return: The Input with the given name or index
        """
        return self.__inputs[name]

    def inputs(self):
        """  Get a list of all Inputs for this slot

        Return: A list of all Inputs for this slot
        """
        return list(self.__inputs.values())

    def add_output(self, outp, /, **kwargs):
        """ Add an output to this slot

        If outp is an Output, add it.  Otherwise create an Output from
        remaining parameters.
        """
        if not isinstance(outp, Output):
            outp = Output(outp, self, **kwargs)
        self.__outputs[outp.name] = outp

    def output(self, name):
        """ Get an Output for this slot

        Return: The Output with the given name or index
        """
        return self.__outputs[name]

    def outputs(self):
        """  Get a list of all Outputs for this slot

        Return: A list of all Outputs for this slot
        """
        return list(self.__outputs.values())

    def allocate_cbufs(self, cbufs=None, length=None, nbufs=1, uram=True, align=0x8000):
        """ Evenly allocate cbuf memory to the selected cbufs within
            the memory for this slot
        """
        with self.rdr_ctrl.cached() as rdr_cache:
            if cbufs is None:
                cbufs = self.cbufs()

            phy_size = self.moku.CBUF_RAM_LENGTH // self.moku.num_slots
            phy_offset = phy_size * self.slot

            if length is None:
                length = (phy_size // len(cbufs)) // nbufs
            else:
                # add space for header
                length = length + self.moku.AXI_BUS_WIDTH * 16

            length = int(math.ceil(float(length) / 0x100) * 0x100)  # rounding/padding

            assert length * nbufs * len(cbufs) <= phy_size, f'not enough phy_mem for length {length:d}'

            uram = uram and self.moku.URAM_LENGTH and (length * nbufs) <= self.moku.URAM_LENGTH

            stop = phy_offset
            for cbuf in cbufs:
                if uram:
                    start = 0
                else:
                    start = int(math.ceil(stop / align) * align)

                stop = start + length * nbufs
                rdr_cache.update(cbuf,
                                 uram=uram,
                                 bounds=[start, stop, length])

    def get_arb(self, idx, length=128):
        data = self._arb_data.readblock(idx, length)
        if data is None:
            return
        return json.loads(data.strip(b'\0'))

    def set_arb(self, idx, data, length=128):
        """ Write a value to the slot scratchpad

        For legacy reasons, the length of data must be known when reading the
        value, so it is padded to a fixed length.
        """
        data = json.dumps(data).ljust(length, '\0').encode()
        if len(data) > length:
            log.warning(f'arb data too long: {len(data)} bytes')
            return
        self._arb_data.writeblock(idx, data)
