from __future__ import annotations

import ctypes
import subprocess
import tempfile
import shutil
from os import path
import platform
import sys
import _ctypes

from .core import working_block, Block
from .wire import Input, Output, Const, WireVector, Register
from .memory import MemBlock, RomBlock
from .pyrtlexceptions import PyrtlError, PyrtlInternalError
from .simulation import SimulationTrace, _trace_sort_key
from .helperfuncs import infer_val_and_bitwidth

try:
    from collections.abc import Mapping
except ImportError:
    from collections import Mapping


__all__ = ['CompiledSimulation']


class DllMemInspector(Mapping):
    """ Dictionary-like access to a hashmap in a CompiledSimulation. """

    def __init__(self, sim, mem):
        self._aw = mem.addrwidth
        self._limbs = sim._limbs(mem)
        self._vn = vn = sim.varname[mem]
        self._mem = ctypes.c_void_p.in_dll(sim._dll, vn)
        self._sim = sim  # keep reference to avoid freeing dll

    def __getitem__(self, ind):
        arr = self._sim._mem_lookup(self._mem, ind)
        val = 0
        for n in reversed(range(self._limbs)):
            val <<= 64
            val |= arr[n]
        return val

    def __iter__(self):
        return iter(range(len(self)))

    def __len__(self):
        return 1 << self._aw

    def __eq__(self, other):
        if isinstance(other, DllMemInspector):
            if self._sim is other._sim and self._vn == other._vn:
                return True
        return all(self[x] == other.get(x, 0) for x in self)


class CompiledSimulation(object):
    """Simulate a block, compiling to C for efficiency.

    This module provides significant speed improvements over
    :class:`.FastSimulation`, at the cost of somewhat longer setup time.
    Generally this will do better than :class:`.FastSimulation` for simulations
    requiring over 1000 steps.  It is not built to be a debugging tool, though
    it may help with debugging.  Note that only :class:`.Input` and
    :class:`.Output` wires can be traced using CompiledSimulation.  This code
    is still experimental, but has been used on designs of significant scale to
    good effect.

    In order to use this, you need:
        - A 64-bit processor
        - GCC (tested on version 4.8.4)
        - A 64-bit build of Python

    If using the multiplication operand, only some architectures are supported:
        - x86-64 / amd64
        - arm64 / aarch64
        - mips64 (untested)

    ``default_value`` is currently only implemented for registers, not memories.

    A Simulation step works as follows:

    1. Registers are updated:
        1. (If this is the first step) With the default values passed in
           to the Simulation during instantiation and/or any reset values
           specified in the individual registers.
        2. (Otherwise) With their next values calculated in the previous step
           (``r`` logic nets).
    2. The new values of these registers as well as the values of block inputs
       are propagated through the combinational logic.
    3. Memory writes are performed (``@`` logic nets).
    4. The current values of all wires are recorded in the trace.
    5. The next values for the registers are saved, ready to be applied at the
       beginning of the next step.

    Note that the register values saved in the trace after each simulation step
    are from *before* the register has latched in its newly calculated values,
    since that latching in occurs at the beginning of the *next* step.

    """

    def __init__(
            self, tracer: SimulationTrace = True,
            register_value_map: dict[Register, int] = {},
            memory_value_map: dict[MemBlock, dict[int, int]] = {},
            default_value: int = 0, block: Block = None):
        self._dll = self._dir = None
        self.block = working_block(block)
        self.block.sanity_check()

        if tracer is True:
            tracer = SimulationTrace()
        self.tracer = tracer
        self._remove_untraceable()

        self.default_value = default_value
        self._regmap = {}  # Updated below
        self._memmap = memory_value_map
        self._uid_counter = 0
        self.varname = {}  # mapping from wires and memories to C variables

        for r in self.block.wirevector_subset(Register):
            rval = register_value_map.get(r, r.reset_value)
            if rval is None:
                rval = self.default_value
            self._regmap[r] = rval

        # Passing the dictionary objects themselves since they aren't updated anywhere.
        # If that's ever not the case, will need to pass in deep copies of them like done
        # for the normal Simulation so we retain the initial values that had.
        self.tracer._set_initial_values(default_value, self._regmap, self._memmap)

        self._create_dll()
        self._initialize_mems()

    def inspect_mem(self, mem: MemBlock) -> dict[int, int]:
        """Get a view into the contents of a MemBlock."""
        return DllMemInspector(self, mem)

    def inspect(self, w: str) -> int:
        """Get the latest value of the wire given, if possible."""
        if isinstance(w, WireVector):
            w = w.name
        try:
            vals = self.tracer.trace[w]
        except KeyError:
            pass
        else:
            if not vals:
                raise PyrtlError('No context available. Please run a simulation step')
            return vals[-1]
        raise PyrtlError('CompiledSimulation does not support inspecting internal WireVectors')

    def step(self, provided_inputs: dict[str, int] = {}, inputs=None):
        """Run one step of the simulation.

        :param provided_inputs: A mapping from input names to the values for
            the step.

        A step causes the block to be updated as follows, in order:

        1. Registers are updated with their :attr:`~.Register.next` values
           computed in the previous cycle
        2. Block inputs and these new register values propagate through the
           combinational logic
        3. Memories are updated
        4. The :attr:`~.Register.next` values of the registers are saved for
           use in step 1 of the next cycle.

        """
        if inputs is not None:
            import warnings
            warnings.warn(
                'CompiledSimulation.step: `inputs` was renamed to '
                '`provided_inputs`', DeprecationWarning)
            provided_inputs = inputs
        self.run([provided_inputs])

    def step_multiple(self, provided_inputs: dict[str, list[int]] = {},
                      expected_outputs: dict[str, int] = {},
                      nsteps: int = None, file=sys.stdout,
                      stop_after_first_error: bool = False):
        """Take the simulation forward N cycles, where N is the number of values
         for each provided input.

        :param provided_inputs: a dictionary mapping wirevectors to their values for N steps
        :param expected_outputs: a dictionary mapping wirevectors to their expected values
            for N steps; use ``?`` to indicate you don't care what the value at that step is
        :param nsteps: number of steps to take (defaults to None, meaning step for each
            supplied input value)
        :param file: where to write the output (if there are unexpected outputs detected)
        :param stop_after_first_error: a boolean flag indicating whether to stop the simulation
            after the step where the first errors are encountered (defaults to False)

        All input wires must be in the `provided_inputs` in order for the simulation
        to accept these values. Additionally, the length of the array of provided values for each
        input must be the same.

        When `nsteps` is specified, then it must be *less than or equal* to the number of values
        supplied for each input when `provided_inputs` is non-empty. When `provided_inputs` is
        empty (which may be a legitimate case for a design that takes no inputs), then `nsteps`
        will be used.  When `nsteps` is not specified, then the simulation will take the number
        of steps equal to the number of values supplied for each input.

        Example: if we have inputs named ``a`` and ``b`` and output ``o``, we can call::

            sim.step_multiple({'a': [0,1], 'b': [23,32]}, {'o': [42, 43]})

        to simulate 2 cycles, where in the first cycle ``a`` and ``b`` take on
        0 and 23, respectively, and ``o`` is expected to have the value 42, and
        in the second cycle ``a`` and ``b`` take on 1 and 32, respectively, and
        ``o`` is expected to have the value 43.

        If your values are all single digit, you can also specify them in a single string, e.g.::

            sim.step_multiple({'a': '01', 'b': '01'})

        will simulate 2 cycles, with ``a`` and ``b`` taking on
        0 and 0, respectively, on the first cycle and 1 and 1, respectively, on the second
        cycle.

        Example: if the design had no inputs, like so::

            a = pyrtl.Register(8)
            b = pyrtl.Output(8, 'b')

            a.next <<= a + 1
            b <<= a

            sim = pyrtl.Simulation()
            sim.step_multiple(nsteps=3)

        Using ``sim.step_multiple(nsteps=3)`` simulates 3 cycles, after which
        we would expect the value of ``b`` to be 2.

        """

        if not nsteps and len(provided_inputs) == 0:
            raise PyrtlError('need to supply either input values or a number of steps to simulate')

        if len(provided_inputs) > 0:
            longest = sorted(list(provided_inputs.items()),
                             key=lambda t: len(t[1]),
                             reverse=True)[0]
            msteps = len(longest[1])
            if nsteps:
                if (nsteps > msteps):
                    raise PyrtlError('nsteps is specified but is greater than the '
                                     'number of values supplied for each input')
            else:
                nsteps = msteps

        if nsteps < 1:
            raise PyrtlError("must simulate at least one step")

        if list(filter(lambda value: len(value) < nsteps,
                       provided_inputs.values())):
            raise PyrtlError(
                "must supply a value for each provided wire "
                "for each step of simulation")

        if list(filter(lambda value: len(value) < nsteps,
                       expected_outputs.values())):
            raise PyrtlError(
                "any expected outputs must have a supplied value "
                "each step of simulation")

        failed = []
        for i in range(nsteps):
            self.step({w: int(v[i]) for w, v in provided_inputs.items()})

            for expvar in expected_outputs.keys():
                expected = expected_outputs[expvar][i]
                if expected == '?':
                    continue
                expected = int(expected)
                actual = self.inspect(expvar)
                if expected != actual:
                    failed.append((i, expvar, expected, actual))

            if failed and stop_after_first_error:
                break

        if failed:
            if stop_after_first_error:
                s = "(stopped after step with first error):"
            else:
                s = "on one or more steps:"
            file.write("Unexpected output " + s + "\n")
            file.write("{0:>5} {1:>10} {2:>8} {3:>8}\n"
                       .format("step", "name", "expected", "actual"))

            def _sort_tuple(t):
                # Sort by step and then wire name
                return (t[0], _trace_sort_key(t[1]))

            failed_sorted = sorted(failed, key=_sort_tuple)
            for (step, name, expected, actual) in failed_sorted:
                file.write("{0:>5} {1:>10} {2:>8} {3:>8}\n".format(step, name, expected, actual))
            file.flush()

    def run(self, inputs: list[dict[str, int]]):
        """ Run many steps of the simulation.

        :param inputs: A list of input mappings for each step;
            its length is the number of steps to be executed.
        """
        steps = len(inputs)
        # create i/o arrays of the appropriate length
        ibuf_type = ctypes.c_uint64 * (steps * self._ibufsz)
        obuf_type = ctypes.c_uint64 * (steps * self._obufsz)
        ibuf = ibuf_type()
        obuf = obuf_type()
        # these array will be passed to _crun
        self._crun.argtypes = [ctypes.c_uint64, ibuf_type, obuf_type]

        # build the input array
        for n, inmap in enumerate(inputs):
            for w in inmap:
                if isinstance(w, WireVector):
                    name = w.name
                else:
                    name = w
                start, count = self._inputpos[name]
                start += n * self._ibufsz
                val = inmap[w]
                val = infer_val_and_bitwidth(
                    val, bitwidth=self._inputbw[name]).value
                # pack input
                for pos in range(start, start + count):
                    ibuf[pos] = val & ((1 << 64) - 1)
                    val >>= 64

        # run the simulation
        self._crun(steps, ibuf, obuf)

        # save traced wires
        for name in self.tracer.trace:
            rname = self._probe_mapping.get(name, name)
            if rname in self._outputpos:
                start, count = self._outputpos[rname]
                buf, sz = obuf, self._obufsz
            elif rname in self._inputpos:
                start, count = self._inputpos[rname]
                buf, sz = ibuf, self._ibufsz
            else:
                raise PyrtlInternalError('Untraceable wire in tracer')
            res = []
            for n in range(steps):
                val = 0
                # unpack output
                for pos in reversed(range(start, start + count)):
                    val <<= 64
                    val |= buf[pos]
                res.append(val)
                start += sz
            self.tracer.trace[name].extend(res)

    def _traceable(self, wv):
        """ Check if wv is able to be traced.

        If it is traceable due to a probe, record that probe in _probe_mapping.
        """
        if isinstance(wv, (Input, Output)):
            return True
        for net in self.block.logic:
            if net.op == 'w' and net.args[0].name == wv.name and isinstance(net.dests[0], Output):
                self._probe_mapping[wv.name] = net.dests[0].name
                return True
        return False

    def _remove_untraceable(self):
        """ Remove from the tracer those wires that CompiledSimulation cannot track.

        Create _probe_mapping for wires only traceable via probes.
        """
        self._probe_mapping = {}
        wvs = {wv for wv in self.tracer.wires_to_track if self._traceable(wv)}
        self.tracer.wires_to_track = wvs
        self.tracer._wires = {wv.name: wv for wv in wvs}
        self.tracer.trace.__init__(wvs)

    def _create_dll(self):
        """ Create a dynamically-linked library implementing the simulation logic. """
        self._dir = tempfile.mkdtemp()
        with open(path.join(self._dir, 'pyrtlsim.c'), 'w') as f:
            self._create_code(lambda s: f.write(s + '\n'))
        if platform.system() == 'Darwin':
            shared = '-dynamiclib'
            march = ''
        else:
            shared = '-shared'
            march = '-march=native'

        subprocess.check_call(['gcc', '-O0', march, '-std=c99', '-m64',
                               shared, '-fPIC',
                               path.join(self._dir, 'pyrtlsim.c'),
                               '-o', path.join(self._dir, 'pyrtlsim.so'), ],
                              shell=(platform.system() == 'Windows'))
        self._dll = ctypes.CDLL(path.join(self._dir, 'pyrtlsim.so'))
        self._crun = self._dll.sim_run_all
        self._crun.restype = None  # argtypes set on use
        self._initialize_mems = self._dll.initialize_mems
        self._initialize_mems.restype = None
        self._mem_lookup = self._dll.lookup
        self._mem_lookup.restype = ctypes.POINTER(ctypes.c_uint64)

    def _limbs(self, w):
        """ Number of 64-bit words needed to store value of wire. """
        return (w.bitwidth + 63) // 64

    def _makeini(self, w, v):
        """ C initializer string for a wire with a given value. """
        pieces = []
        for _ in range(self._limbs(w)):
            pieces.append(hex(v & ((1 << 64) - 1)))
            v >>= 64
        return ','.join(pieces).join('{}')

    def _romwidth(self, m):
        """ Bitwidth of integer type sufficient to hold rom entry.

        On large memories, returns 64; an array will be needed.
        """
        if m.bitwidth <= 8:
            return 8
        if m.bitwidth <= 16:
            return 16
        if m.bitwidth <= 32:
            return 32
        return 64

    def _makemask(self, dest, res, pos):
        """Create a bitmask.

        The value being masked is of width `res`.
        Limb number `pos` of `dest` is being assigned to.
        """
        if (res is None or dest.bitwidth < res) and 0 < (dest.bitwidth - 64 * pos) < 64:
            return '&0x{:X}'.format((1 << (dest.bitwidth % 64)) - 1)
        return ''

    def _getarglimb(self, arg, n):
        """ Get the nth limb of the given wire.

        Returns '0' when the wire does not have sufficient limbs.
        """
        return '{vn}[{n}]'.format(vn=self.varname[arg], n=n) if arg.bitwidth > 64 * n else '0'

    def _clean_name(self, prefix, obj):
        """ Create a C variable name with the given prefix based on the name of obj. """
        return '{}{}_{}'.format(prefix, self._uid(), ''.join(c for c in obj.name if c.isalnum()))

    def _uid(self):
        """ Get an auto-incrementing number suitable for use as a unique identifier. """
        x = self._uid_counter
        self._uid_counter += 1
        return x

    def _declare_roms(self, write, roms):
        for mem in roms:
            self.varname[mem] = vn = self._clean_name('m', mem)
            # extract data from mem
            romval = [mem._get_read_data(n) for n in range(1 << mem.addrwidth)]
            write('static const uint{width}_t {name}[][{limbs}] = {{'.format(
                name=vn, width=self._romwidth(mem), limbs=self._limbs(mem)))
            for rv in romval:
                write(self._makeini(mem, rv) + ',')
            write('};')

    def _declare_mems(self, write, mems):
        for mem in mems:
            self.varname[mem] = vn = self._clean_name('m', mem)
            write('EXPORT')
            write('hashmap_t *{name};'.format(name=vn))

        next_tmp = 0
        write('EXPORT')
        write('void initialize_mems() {')
        for mem in mems:
            # Create hashmap
            write('{name} = create_hash_map(256, {limbs});'.format(
                name=self.varname[mem], limbs=self._limbs(mem)
            ))
            if mem in self._memmap:
                # Insert default values
                for k, v in self._memmap[mem].items():
                    write('val_t t{n}[] = {val};'.format(
                        n=next_tmp, val=self._makeini(mem, v)))
                    write('insert({name}, {key}, t{n});'.format(
                        name=self.varname[mem], key=k, n=next_tmp
                    ))
                    next_tmp += 1
        write('}')

    def _declare_wv(self, write, w):
        self.varname[w] = vn = self._clean_name('w', w)
        if isinstance(w, Const):
            write('const uint64_t {name}[{limbs}] = {val};'.format(
                limbs=self._limbs(w), name=vn, val=self._makeini(w, w.val)))
        elif isinstance(w, Register):
            rval = self._regmap.get(w, w.reset_value)
            if rval is None:
                rval = self.default_value
            write('static uint64_t {name}[{limbs}] = {val};'.format(
                limbs=self._limbs(w), name=vn, val=self._makeini(w, rval)))
        else:
            write('uint64_t {name}[{limbs}];'.format(limbs=self._limbs(w), name=vn))

    def _build_memread(self, write, op, param, args, dest):
        mem = param[1]
        for n in range(self._limbs(dest)):
            if isinstance(mem, RomBlock):
                write('{dest}[{n}] = {mem}[{addr}[0]][{n}]{mask};'.format(
                    dest=self.varname[dest], n=n, mem=self.varname[mem],
                    addr=self.varname[args[0]], mask=self._makemask(dest, mem.bitwidth, n)))
            else:
                write('{dest}[{n}] = lookup({mem}, {addr}[0])[{n}]{mask};'.format(
                    dest=self.varname[dest], n=n, mem=self.varname[mem],
                    addr=self.varname[args[0]], mask=self._makemask(dest, mem.bitwidth, n)))

    def _build_wire(self, write, op, param, args, dest):
        for n in range(self._limbs(dest)):
            write('{dest}[{n}] = {arg}[{n}]{mask};'.format(
                dest=self.varname[dest], n=n, arg=self.varname[args[0]],
                mask=self._makemask(dest, args[0].bitwidth, n)))

    def _build_not(self, write, op, param, args, dest):
        for n in range(self._limbs(dest)):
            write('{dest}[{n}] = (~{arg}[{n}]){mask};'.format(
                dest=self.varname[dest], n=n, arg=self.varname[args[0]],
                mask=self._makemask(dest, None, n)))

    def _build_bitwise(self, write, op, param, args, dest):  # &, |, ^ only
        for n in range(self._limbs(dest)):
            arg0 = self._getarglimb(args[0], n)
            arg1 = self._getarglimb(args[1], n)
            write('{dest}[{n}] = ({arg0}{op}{arg1}){mask};'.format(
                dest=self.varname[dest], n=n, arg0=arg0, arg1=arg1, op=op,
                mask=self._makemask(dest, max(args[0].bitwidth, args[1].bitwidth), n)))

    def _build_nand(self, write, op, param, args, dest):
        for n in range(self._limbs(dest)):
            arg0 = self._getarglimb(args[0], n)
            arg1 = self._getarglimb(args[1], n)
            write('{dest}[{n}] = (~({arg0}&{arg1})){mask};'.format(
                dest=self.varname[dest], n=n, arg0=arg0, arg1=arg1,
                mask=self._makemask(dest, None, n)))

    def _build_eq(self, write, op, param, args, dest):
        cond = []
        for n in range(max(self._limbs(args[0]), self._limbs(args[1]))):
            arg0 = self._getarglimb(args[0], n)
            arg1 = self._getarglimb(args[1], n)
            cond.append('({arg0}=={arg1})'.format(arg0=arg0, arg1=arg1))
        write('{dest}[0] = {cond};'.format(dest=self.varname[dest], cond='&&'.join(cond)))

    def _build_cmp(self, write, op, param, args, dest):  # <, > only
        cond = None
        for n in range(max(self._limbs(args[0]), self._limbs(args[1]))):
            arg0 = self._getarglimb(args[0], n)
            arg1 = self._getarglimb(args[1], n)
            c = '({arg0}{op}{arg1})'.format(arg0=arg0, op=op, arg1=arg1)
            if cond is None:
                cond = c
            else:
                cond = '({c}||(({arg0}=={arg1})&&{inner}))'.format(
                    c=c, arg0=arg0, arg1=arg1, inner=cond)
        write('{dest}[0] = {cond};'.format(dest=self.varname[dest], cond=cond))

    def _build_mux(self, write, op, param, args, dest):
        write('if ({mux}[0]) {{'.format(mux=self.varname[args[0]]))
        for n in range(self._limbs(dest)):
            write('{dest}[{n}] = {arg}[{n}]{mask};'.format(
                dest=self.varname[dest], n=n, arg=self.varname[args[2]],
                mask=self._makemask(dest, args[2].bitwidth, n)))
        write('} else {')
        for n in range(self._limbs(dest)):
            write('{dest}[{n}] = {arg}[{n}]{mask};'.format(
                dest=self.varname[dest], n=n, arg=self.varname[args[1]],
                mask=self._makemask(dest, args[1].bitwidth, n)))
        write('}')

    def _build_add(self, write, op, param, args, dest):
        write('carry = 0;')
        for n in range(self._limbs(dest)):
            arg0 = self._getarglimb(args[0], n)
            arg1 = self._getarglimb(args[1], n)
            write('tmp = {arg0}+{arg1};'.format(arg0=arg0, arg1=arg1))
            write('{dest}[{n}] = (tmp + carry){mask};'.format(
                dest=self.varname[dest], n=n,
                mask=self._makemask(dest, max(args[0].bitwidth, args[1].bitwidth) + 1, n)))
            write('carry = (tmp < {arg0})|({dest}[{n}] < tmp);'.format(
                arg0=arg0, dest=self.varname[dest], n=n))

    def _build_sub(self, write, op, param, args, dest):
        write('carry = 0;')
        for n in range(self._limbs(dest)):
            arg0 = self._getarglimb(args[0], n)
            arg1 = self._getarglimb(args[1], n)
            write('tmp = {arg0}-{arg1};'.format(arg0=arg0, arg1=arg1))
            write('{dest}[{n}] = (tmp - carry){mask};'.format(
                dest=self.varname[dest], n=n, mask=self._makemask(dest, None, n)))
            write('carry = (tmp > {arg0})|({dest}[{n}] > tmp);'.format(
                arg0=arg0, dest=self.varname[dest], n=n))

    def _build_mul(self, write, op, param, args, dest):
        for n in range(self._limbs(dest)):
            write('{dest}[{n}] = 0;'.format(dest=self.varname[dest], n=n))
        for p0 in range(self._limbs(args[0])):
            write('carry = 0;')
            arg0 = self._getarglimb(args[0], p0)
            for p1 in range(self._limbs(args[1])):
                if self._limbs(dest) <= p0 + p1:
                    break
                arg1 = self._getarglimb(args[1], p1)
                write('mul128({arg0}, {arg1}, tmplo, tmphi);'.format(arg0=arg0, arg1=arg1))
                write('tmp = {dest}[{p}];'.format(dest=self.varname[dest], p=p0 + p1))
                write('tmplo += carry; carry = tmplo < carry; tmplo += tmp;')
                write('tmphi += carry + (tmplo < tmp); carry = tmphi;')
                write('{dest}[{p}] = tmplo{mask};'.format(
                    dest=self.varname[dest], p=p0 + p1,
                    mask=self._makemask(dest, args[0].bitwidth + args[1].bitwidth, p0 + p1)))
            if self._limbs(dest) > p0 + self._limbs(args[1]):
                write('{dest}[{p}] = carry{mask};'.format(
                    dest=self.varname[dest], p=p0 + self._limbs(args[1]),
                    mask=self._makemask(
                        dest, args[0].bitwidth + args[1].bitwidth, p0 + self._limbs(args[1]))))

    def _build_concat(self, write, op, param, args, dest):
        cattotal = sum(x.bitwidth for x in args)
        pieces = (
            (self.varname[a], lx, 0, min(64, a.bitwidth - 64 * lx))
            for a in reversed(args) for lx in range(self._limbs(a)))
        curr = next(pieces)
        for n in range(self._limbs(dest)):
            res = []
            dpos = 0
            while True:
                arg, alimb, astart, asize = curr
                res.append('(({arg}[{limb}]>>{start})<<{pos})'.format(
                    arg=arg, limb=alimb, start=astart, pos=dpos))
                dpos += asize
                if dpos >= dest.bitwidth - 64 * n:
                    break
                if dpos > 64:
                    curr = (arg, alimb, 64 - (dpos - asize), dpos - 64)
                    break
                curr = next(pieces)
                if dpos == 64:
                    break
            write('{dest}[{n}] = ({res}){mask};'.format(
                dest=self.varname[dest], n=n, res='|'.join(res),
                mask=self._makemask(dest, cattotal, n)))

    def _build_select(self, write, op, param, args, dest):
        for n in range(self._limbs(dest)):
            bits = [
                '((1&({src}[{limb}]>>{sb}))<<{db})'.format(
                    src=self.varname[args[0]], sb=(b % 64), limb=(b // 64), db=en)
                for en, b in enumerate(param[64 * n:min(dest.bitwidth, 64 * (n + 1))])]
            write('{dest}[{n}] = {bits};'.format(
                dest=self.varname[dest], n=n, bits='|'.join(bits)))

    def _declare_mem_helpers(self, write):
        helpers = '''
            typedef uint64_t val_t;

            typedef struct node
            {
                uint64_t key;
                val_t *val;
                struct node *next;
            } node_t;

            typedef struct hashmap
            {
                int size;
                int val_limbs;
                val_t *default_value;
                node_t **list;
            } hashmap_t;

            hashmap_t *create_hash_map(int size, int val_limbs)
            {
                int i;
                hashmap_t *h = (hashmap_t *) malloc(sizeof(hashmap_t));
                h->size = size;
                h->val_limbs = val_limbs;
                h->list = (node_t **) malloc(sizeof(node_t *) * size);
                h->default_value = (val_t *) malloc(sizeof(val_t) * val_limbs);
                for (i = 0; i < val_limbs; i++)
                    h->default_value[i] = 0;
                for (i = 0; i < size; i++)
                    h->list[i] = NULL;
                return h;
            }

            int hash_code(hashmap_t *h, uint64_t key)
            {
                return key % h->size;
            }

            void insert(hashmap_t *h, uint64_t key, val_t val[])
            {
                int pos = hash_code(h, key);
                struct node *list = h->list[pos];
                struct node *new_node = (node_t *) malloc(sizeof(node_t));
                struct node *temp = list;
                while (temp)
                {
                    if (temp->key == key)
                    {
                        memcpy(temp->val, val, sizeof(val_t) * h->val_limbs);
                        return;
                    }
                    temp = temp->next;
                }
                new_node->key = key;
                new_node->val = (val_t *) malloc(sizeof(val_t) * h->val_limbs);
                memcpy(new_node->val, val, sizeof(val_t) * h->val_limbs);
                new_node->next = list;
                h->list[pos] = new_node;
            }

            EXPORT
            val_t* lookup(hashmap_t *h, uint64_t key)
            {
                int pos = hash_code(h, key);
                node_t *list = h->list[pos];
                node_t *temp = list;
                while (temp)
                {
                    if (temp->key == key)
                    {
                        return temp->val;
                    }
                    temp = temp->next;
                }
                return h->default_value;
            }
        '''
        write(helpers)

    def _create_code(self, write):
        write('#include <stdint.h>')
        write('#include <stdlib.h>')
        write('#include <string.h>')

        # windows dllexport needed to make symbols visible
        if platform.system() == 'Windows':
            write('#define EXPORT __declspec(dllexport)')
        else:
            write('#define EXPORT')

        # multiplication macro
        #  for efficient 64x64 -> 128 bit multiplication without uint128_t
        #  as -O0 optimization does not handle uint128_t well
        machine_alias = {'amd64': 'x86_64', 'aarch64': 'arm64', 'aarch64_be': 'arm64'}
        machine = platform.machine().lower()
        machine = machine_alias.get(machine, machine)
        mulinstr = {
            'x86_64': '"mulq %q3":"=a"(pl),"=d"(ph):"%0"(t0),"r"(t1):"cc"',
            'arm64': '"mul %0, %2, %3\\n\\t" \\\n'
                     '"umulh %1, %2, %3":"=&r"(pl),"=r"(ph):"r"(t0),"r"(t1):"cc"',
            'mips64': '"dmultu %2, %3\\n\\t" \\\n'
                      '"tmflo %0\\n\\t" \\\n'
                      '"mfhi %1":"=r"(pl),"=r"(ph):"r"(t0),"r"(t1)',
        }
        if machine in mulinstr:
            write('#define mul128(t0, t1, pl, ph) __asm__({})'.format(mulinstr[machine]))

        # declare memories
        mems = {net.op_param[1] for net in self.block.logic_subset('m@')}
        for key in self._memmap:
            if key not in mems:
                raise PyrtlError('unrecognized MemBlock in memory_value_map')
            if isinstance(key, RomBlock):
                raise PyrtlError('RomBlock in memory_value_map')
        self._declare_mem_helpers(write)
        roms = {mem for mem in mems if isinstance(mem, RomBlock)}
        self._declare_roms(write, roms)
        mems = {mem for mem in mems if isinstance(mem, MemBlock) and not isinstance(mem, RomBlock)}
        self._declare_mems(write, mems)

        # single step function
        write('static void sim_run_step(uint64_t inputs[], uint64_t outputs[]) {')
        write('uint64_t tmp, carry, tmphi, tmplo;')  # temporary variables

        # declare wire vectors
        for w in self.block.wirevector_set:
            self._declare_wv(write, w)

        # inputs copied in
        inputs = list(self.block.wirevector_subset(Input))
        self._inputpos = {}  # for each input wire, start and number of elements in input array
        self._inputbw = {}  # bitwidth of each input wire
        ipos = 0
        for w in inputs:
            self._inputpos[w.name] = ipos, self._limbs(w)
            self._inputbw[w.name] = w.bitwidth
            for n in range(self._limbs(w)):
                write('{vn}[{n}] = inputs[{pos}];'.format(vn=self.varname[w], n=n, pos=ipos))
                ipos += 1
        self._ibufsz = ipos  # total length of input array

        # combinational logic
        op_builders = {
            'm': self._build_memread,
            'w': self._build_wire,
            '~': self._build_not,
            '&': self._build_bitwise,
            '|': self._build_bitwise,
            '^': self._build_bitwise,
            'n': self._build_nand,
            '=': self._build_eq,
            '<': self._build_cmp,
            '>': self._build_cmp,
            'x': self._build_mux,
            '+': self._build_add,
            '-': self._build_sub,
            '*': self._build_mul,
            'c': self._build_concat,
            's': self._build_select,
        }
        for net in self.block:  # topological order
            if net.op in 'r@':
                continue  # skip synchronized nets
            op, param, args, dest = net.op, net.op_param, net.args, net.dests[0]
            write('// net {op} : {args} -> {dest}'.format(
                op=op, args=', '.join(self.varname[x] for x in args), dest=self.varname[dest]))
            op_builders[op](write, op, param, args, dest)

        # memory writes
        for net in self.block.logic_subset('@'):
            mem = net.op_param[1]
            write('if ({enable}[0]) {{'.format(enable=self.varname[net.args[2]]))
            write('insert({mem}, {addr}[0], {vn});'.format(
                mem=self.varname[mem],
                addr=self.varname[net.args[0]],
                vn=self.varname[net.args[1]]
            ))
            write('}')

        # register updates
        regnets = list(self.block.logic_subset('r'))
        for x, net in enumerate(regnets):
            rin = net.args[0]
            write('uint64_t regtmp{x}[{limbs}];'.format(x=x, limbs=self._limbs(rin)))
            for n in range(self._limbs(rin)):
                write('regtmp{x}[{n}] = {vn}[{n}];'.format(x=x, vn=self.varname[rin], n=n))
        # double loop to ensure register-to-register chains update correctly
        for x, net in enumerate(regnets):
            rout = net.dests[0]
            for n in range(self._limbs(rout)):
                write('{vn}[{n}] = regtmp{x}[{n}];'.format(vn=self.varname[rout], x=x, n=n))

        # output copied out
        outputs = list(self.block.wirevector_subset(Output))
        self._outputpos = {}  # for each output wire, start and number of elements in output array
        opos = 0
        for w in outputs:
            self._outputpos[w.name] = opos, self._limbs(w)
            for n in range(self._limbs(w)):
                write('outputs[{pos}] = {vn}[{n}];'.format(pos=opos, vn=self.varname[w], n=n))
                opos += 1
        self._obufsz = opos  # total length of output array
        write('}')

        # entry point
        write('EXPORT')
        write('void sim_run_all(uint64_t stepcount, uint64_t inputs[], uint64_t outputs[]) {')
        write('uint64_t input_pos = 0, output_pos = 0;')
        write('for (uint64_t stepnum = 0; stepnum < stepcount; stepnum++) {')
        write('sim_run_step(inputs+input_pos, outputs+output_pos);')
        write('input_pos += {};'.format(self._ibufsz))
        write('output_pos += {};'.format(self._obufsz))
        write('}}')

    def __del__(self):
        """Handle removal of the DLL when the simulator is deleted."""
        if self._dll is not None:
            handle = self._dll._handle
            if platform.system() == 'Windows':
                _ctypes.FreeLibrary(handle)  # pylint: disable=no-member
            else:
                _ctypes.dlclose(handle)  # pylint: disable=no-member
            self._dll = None
        if self._dir is not None:
            shutil.rmtree(self._dir)
            self._dir = None
