"""
Helper functions for reading and writing hardware files.

Each of the functions in inputoutput take a block and a file descriptor.
The functions provided either read the file and update the Block
accordingly, or write information from the Block out to the file.
"""

from __future__ import print_function, unicode_literals
import re
import collections

from .pyrtlexceptions import PyrtlError, PyrtlInternalError
from .core import working_block, _NameSanitizer
from .wire import WireVector, Input, Output, Const, Register
from .corecircuits import concat_list
from .memory import RomBlock
from .passes import two_way_concat, one_bit_selects


# -----------------------------------------------------------------
#            __       ___
#    | |\ | |__) |  |  |
#    | | \| |    \__/  |


def input_from_blif(blif, block=None, merge_io_vectors=True, clock_name='clk'):
    """ Read an open blif file or string as input, updating the block appropriately

    Assumes the blif has been flattened and their is only a single module.
    Assumes that there is only one single shared clock and reset
    Assumes that output is generated by Yosys with formals in a particular order
    Ignores reset signal (which it assumes is input only to the flip flops)
    """
    import pyparsing
    import six
    from pyparsing import (Word, Literal, OneOrMore, ZeroOrMore,
                           Suppress, Group, Keyword, Optional, oneOf)

    block = working_block(block)

    try:
        blif_string = blif.read()
    except AttributeError:
        if isinstance(blif, six.string_types):
            blif_string = blif
        else:
            raise PyrtlError('input_blif expecting either open file or string')

    def SKeyword(x):
        return Suppress(Keyword(x))

    def SLiteral(x):
        return Suppress(Literal(x))

    def twire(x):
        """ find or make wire named x and return it """
        s = block.get_wirevector_by_name(x)
        if s is None:
            s = WireVector(bitwidth=1, name=x)
        if isinstance(s, Output) and (merge_io_vectors or len(x) == 1):
            # To allow an output wire to be used as an argument (legal in BLIF),
            # use the intermediate wire that was created in its place. extract_outputs()
            # creates this intermediate wire.
            s = block.get_wirevector_by_name(x + '[0]')
        return s

    # Begin BLIF language definition
    signal_start = pyparsing.alphas + r'$:[]_<>\\\/?'
    signal_middle = pyparsing.alphas + pyparsing.nums + r'$:[]_<>\\\/.?'
    signal_id = Word(signal_start, signal_middle)
    header = SKeyword('.model') + signal_id('model_name')
    input_list = Group(SKeyword('.inputs') + OneOrMore(signal_id))('input_list')
    output_list = Group(SKeyword('.outputs') + OneOrMore(signal_id))('output_list')

    cover_atom = Word('01-')
    cover_list = Group(ZeroOrMore(cover_atom))('cover_list')
    namesignal_list = Group(OneOrMore(signal_id))('namesignal_list')
    name_def = Group(SKeyword('.names') + namesignal_list + cover_list)('name_def')

    # asynchronous Flip-flop
    dffas_formal = (SLiteral('C=') + signal_id('C')
                    + SLiteral('R=') + signal_id('R')
                    + SLiteral('D=') + signal_id('D')
                    + SLiteral('Q=') + signal_id('Q'))
    dffas_keyword = SKeyword('$_DFF_PN0_') | SKeyword('$_DFF_PP0_')
    dffas_def = Group(SKeyword('.subckt') + dffas_keyword + dffas_formal)('dffas_def')

    # synchronous Flip-flop
    dffs_init_val = Optional(oneOf("0 1 2 3"), default=Literal("0"))
    # TODO I think <type> and <control> ('re' and 'C') below are technically optional too
    dffs_def = Group(SKeyword('.latch')
                     + signal_id('D')
                     + signal_id('Q')
                     + SLiteral('re')
                     + signal_id('C')
                     + dffs_init_val('I'))('dffs_def')
    command_def = name_def | dffas_def | dffs_def
    command_list = Group(OneOrMore(command_def))('command_list')

    footer = SKeyword('.end')
    model_def = Group(header + input_list + output_list + command_list + footer)
    model_list = OneOrMore(model_def)
    parser = model_list.ignore(pyparsing.pythonStyleComment)

    # Begin actually reading and parsing the BLIF file
    result = parser.parseString(blif_string, parseAll=True)
    # Blif file with multiple models (currently only handles one flattened models)
    assert(len(result) == 1)
    clk_set = set([])
    ff_clk_set = set([])

    def extract_inputs(model):
        start_names = [re.sub(r'\[([0-9]+)\]$', '', x) for x in model['input_list']]
        name_counts = collections.Counter(start_names)
        for input_name in name_counts:
            bitwidth = name_counts[input_name]
            if input_name == clock_name:
                clk_set.add(input_name)
            elif not merge_io_vectors or bitwidth == 1:
                block.add_wirevector(Input(bitwidth=1, name=input_name))
            else:
                wire_in = Input(bitwidth=bitwidth, name=input_name, block=block)
                for i in range(bitwidth):
                    bit_name = input_name + '[' + str(i) + ']'
                    bit_wire = WireVector(bitwidth=1, name=bit_name, block=block)
                    bit_wire <<= wire_in[i]

    def extract_outputs(model):
        start_names = [re.sub(r'\[([0-9]+)\]$', '', x) for x in model['output_list']]
        name_counts = collections.Counter(start_names)
        for output_name in name_counts:
            bitwidth = name_counts[output_name]
            if not merge_io_vectors or bitwidth == 1:
                # To allow an output wire to be used as an argument (legal in BLIF),
                # create an intermediate wire that will be used in its place. twire()
                # checks for this and uses the intermediate wire when needed
                w = WireVector(bitwidth=1, name=output_name + '[0]')
                out = Output(bitwidth=1, name=output_name)
                out <<= w
            else:
                wire_out = Output(bitwidth=bitwidth, name=output_name, block=block)
                bit_list = []
                for i in range(bitwidth):
                    bit_name = output_name + '[' + str(i) + ']'
                    bit_wire = WireVector(bitwidth=1, name=bit_name, block=block)
                    bit_list.append(bit_wire)
                wire_out <<= concat_list(bit_list)

    def extract_commands(model):
        # for each "command" (dff or net) in the model
        for command in model['command_list']:
            # if it is a net (specified as a cover)
            if command.getName() == 'name_def':
                extract_cover(command)
            # else if the command is a d flop flop
            elif command.getName() == 'dffas_def' or command.getName() == 'dffs_def':
                extract_flop(command)
            else:
                raise PyrtlError('unknown command type')

    def extract_cover(command):
        # pylint: disable=invalid-unary-operand-type
        netio = command['namesignal_list']
        if len(command['cover_list']) == 0:
            output_wire = twire(netio[0])
            output_wire <<= Const(0, bitwidth=1, block=block)  # const "FALSE"
        elif command['cover_list'].asList() == ['1']:
            output_wire = twire(netio[0])
            output_wire <<= Const(1, bitwidth=1, block=block)  # const "TRUE"
        elif command['cover_list'].asList() == ['1', '1']:
            # Populate clock list if one input is already a clock
            if(netio[1] in clk_set):
                clk_set.add(netio[0])
            elif(netio[0] in clk_set):
                clk_set.add(netio[1])
            else:
                output_wire = twire(netio[1])
                output_wire <<= twire(netio[0])  # simple wire
        elif command['cover_list'].asList() == ['0', '1']:
            output_wire = twire(netio[1])
            output_wire <<= ~ twire(netio[0])  # not gate
        elif command['cover_list'].asList() == ['11', '1']:
            output_wire = twire(netio[2])
            output_wire <<= twire(netio[0]) & twire(netio[1])  # and gate
        elif command['cover_list'].asList() == ['00', '1']:
            output_wire = twire(netio[2])
            output_wire <<= ~ (twire(netio[0]) | twire(netio[1]))  # nor gate
        elif command['cover_list'].asList() == ['1-', '1', '-1', '1']:
            output_wire = twire(netio[2])
            output_wire <<= twire(netio[0]) | twire(netio[1])  # or gate
        elif command['cover_list'].asList() == ['10', '1', '01', '1']:
            output_wire = twire(netio[2])
            output_wire <<= twire(netio[0]) ^ twire(netio[1])  # xor gate
        elif command['cover_list'].asList() == ['1-0', '1', '-11', '1']:
            output_wire = twire(netio[3])
            output_wire <<= (twire(netio[0]) & ~ twire(netio[2])) \
                | (twire(netio[1]) & twire(netio[2]))   # mux
        elif command['cover_list'].asList() == ['-00', '1', '0-0', '1']:
            output_wire = twire(netio[3])
            output_wire <<= (~twire(netio[1]) & ~twire(netio[2])) \
                | (~twire(netio[0]) & ~twire(netio[2]))
        else:
            raise PyrtlError('Blif file with unknown logic cover set "%s"'
                             '(currently gates are hard coded)' % command['cover_list'])

    def extract_flop(command):
        if(command['C'] not in ff_clk_set):
            ff_clk_set.add(command['C'])

        # Create register and assign next state to D and output to Q
        regname = command['Q'] + '_reg'
        flop = Register(bitwidth=1, name=regname)
        flop.next <<= twire(command['D'])
        flop_output = twire(command['Q'])
        init_val = command['I']
        if init_val == "1":
            # e.g. in Verilog: `initial reg <= 1;`
            raise PyrtlError("Initializing latches to 1 is not supported. "
                             "Acceptable values are: 0, 2 (don't care), and 3 (unknown); "
                             "in any case, PyRTL will ensure all stateful elements come up 0. "
                             "For finer control over the initial value, use specialized reset "
                             "logic.")
        flop_output <<= flop

    for model in result:
        extract_inputs(model)
        extract_outputs(model)
        extract_commands(model)


# ----------------------------------------------------------------
#    __       ___  __       ___
#   /  \ |  |  |  |__) |  |  |
#   \__/ \__/  |  |    \__/  |
#


def _natural_sort_key(key):
    """ Convert the key into a form such that it will be sorted naturally,
        e.g. such that "tmp4" appears before "tmp18".
    """
    def convert(text):
        return int(text) if text.isdigit() else text
    return [convert(c) for c in re.split(r'(\d+)', key)]


def _net_sorted(logic, name_mapper=lambda w: w.name):
    # Sort nets based on the name of the destination
    # wire, unless it's a memory write net.
    def natural_keys(n):
        if n.op == '@':
            # Sort based on the name of the wr_en wire, since
            # this particular net is used within 'always begin ... end'
            # blocks for memory update logic.
            key = str(n.args[2])
        else:
            key = name_mapper(n.dests[0])
        return _natural_sort_key(key)
    return sorted(logic, key=natural_keys)


def _name_sorted(wires, name_mapper=lambda w: w.name):
    return sorted(wires, key=lambda w: _natural_sort_key(name_mapper(w)))


def output_to_firrtl(open_file, rom_blocks=None, block=None):
    """ Output the block as firrtl code to the output file.

    Output_to_firrtl(open_file, rom_block, block)
    If rom is intialized in pyrtl code, you can pass in the rom_blocks as a list [rom1, rom2, ...]
    """
    block = working_block(block)

    # FIRRTL only allows 'bits' operations to have two parameters: a high and low
    # index representing the inclusive bounds of a contiguous range. PyRTL uses
    # slice syntax, which aren't always contiguous, so we need to convert them.
    one_bit_selects(block=block)  # pylint: disable=no-value-for-parameter,unexpected-keyword-arg

    # FIRRTL only allows 'concatenate' operations to have two arguments,
    # but PyRTL's 'c' op allows an arbitrary number of wires. We need to convert
    # these n-way concats to series of two-way concats accordingly.
    two_way_concat(block=block)  # pylint: disable=no-value-for-parameter,unexpected-keyword-arg

    f = open_file
    # write out all the implicit stuff
    f.write("circuit Example :\n")
    f.write("  module Example :\n")
    f.write("    input clock : Clock\n    input reset : UInt<1>\n")
    # write out IO signals, wires and registers
    for wire in _name_sorted(block.wirevector_subset(Input)):
        f.write("    input %s : UInt<%d>\n" % (wire.name, wire.bitwidth))
    for wire in _name_sorted(block.wirevector_subset(Output)):
        f.write("    output %s : UInt<%d>\n" % (wire.name, wire.bitwidth))
    for wire in _name_sorted(block.wirevector_subset(exclude=(Input, Output, Register, Const))):
        f.write("    wire %s : UInt<%d>\n" % (wire.name, wire.bitwidth))
    for wire in _name_sorted(block.wirevector_subset(Register)):
        f.write("    reg %s : UInt<%d>, clock\n" % (wire.name, wire.bitwidth))
    for wire in _name_sorted(block.wirevector_subset(Const)):
        # some const is in the form like const_0_1'b1, is this legal operation?
        wire.name = wire.name.split("'").pop(0)
        f.write("    node %s = UInt<%d>(%d)\n" % (wire.name, wire.bitwidth, wire.val))
    f.write("\n")

    # write "Main"
    node_cntr = 0
    initializedMem = []
    for log_net in _net_sorted(block.logic_subset()):
        if log_net.op == '&':
            f.write("    %s <= and(%s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                 log_net.args[1].name))
        elif log_net.op == '|':
            f.write("    %s <= or(%s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                log_net.args[1].name))
        elif log_net.op == '^':
            f.write("    %s <= xor(%s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                 log_net.args[1].name))
        elif log_net.op == 'n':
            f.write("    node T_%d = and(%s, %s)\n" % (node_cntr, log_net.args[0].name,
                                                       log_net.args[1].name))
            f.write("    %s <= not(T_%d)\n" % (log_net.dests[0].name, node_cntr))
            node_cntr += 1
        elif log_net.op == '~':
            f.write("    %s <= not(%s)\n" % (log_net.dests[0].name, log_net.args[0].name))
        elif log_net.op == '+':
            f.write("    %s <= add(%s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                 log_net.args[1].name))
        elif log_net.op == '-':
            f.write("    %s <= sub(%s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                 log_net.args[1].name))
        elif log_net.op == '*':
            f.write("    %s <= mul(%s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                 log_net.args[1].name))
        elif log_net.op == '=':
            f.write("    %s <= eq(%s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                log_net.args[1].name))
        elif log_net.op == '<':
            f.write("    %s <= lt(%s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                log_net.args[1].name))
        elif log_net.op == '>':
            f.write("    %s <= gt(%s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                log_net.args[1].name))
        elif log_net.op == 'w':
            f.write("    %s <= %s\n" % (log_net.dests[0].name, log_net.args[0].name))
        elif log_net.op == 'x':
            f.write("    %s <= mux(%s, %s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                     log_net.args[2].name, log_net.args[1].name))
        elif log_net.op == 'c':
            if len(log_net.args) != 2:
                raise PyrtlInternalError(
                    "Expected concat net to have only two "
                    "argument wires; has %d" % len(log_net.args)
                )
            f.write("    %s <= cat(%s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                 log_net.args[1].name))
        elif log_net.op == 's':
            if len(log_net.op_param) != 1:
                raise PyrtlInternalError(
                    "Expected select net to have single "
                    "select bit; has %d" % len(log_net.op_param)
                )
            f.write("    %s <= bits(%s, %s, %s)\n" % (log_net.dests[0].name, log_net.args[0].name,
                                                      log_net.op_param[0], log_net.op_param[0]))
        elif log_net.op == 'r':
            f.write("    %s <= mux(reset, UInt<%s>(0), %s)\n" %
                    (log_net.dests[0].name, log_net.dests[0].bitwidth, log_net.args[0].name))
        elif log_net.op == 'm':
            # if there are rom blocks, need to be initialized
            if rom_blocks is not None:
                if not log_net.op_param[0] in initializedMem:
                    initializedMem.append(log_net.op_param[0])

                    # find corresponding rom block according to memid
                    curr_rom = next((x for x in rom_blocks if x.id == log_net.op_param[0]), None)
                    f.write("    wire %s : UInt<%s>[%s]\n" %
                            (log_net.op_param[1].name, log_net.op_param[1].bitwidth,
                             2**log_net.op_param[1].addrwidth))

                    # if rom data is a function, calculate the data first
                    if callable(curr_rom.data):
                        romdata = [curr_rom.data(i) for i in range(2**curr_rom.addrwidth)]
                        curr_rom.data = romdata

                    # write rom block initialization data
                    for i in range(len(curr_rom.data)):
                        f.write("    %s[%s] <= UInt<%s>(%s)\n" %
                                (log_net.op_param[1].name, i, log_net.op_param[1].bitwidth,
                                 curr_rom.data[i]))

                # write the connection
                f.write("    %s <= %s[%s]\n" % (log_net.dests[0].name, log_net.op_param[1].name,
                                                log_net.args[0].name))

            else:
                if not log_net.op_param[0] in initializedMem:
                    initializedMem.append(log_net.op_param[0])
                    f.write("    cmem %s_%s : UInt<%s>[%s]\n" %
                            (log_net.op_param[1].name, log_net.op_param[0],
                             log_net.op_param[1].bitwidth, 2**log_net.op_param[1].addrwidth))
                f.write("    infer mport T_%d  = %s_%s[%s], clock\n" %
                        (node_cntr, log_net.op_param[1].name, log_net.op_param[0],
                         log_net.args[0].name))
                f.write("    %s <= T_%d\n" % (log_net.dests[0].name, node_cntr))
                node_cntr += 1
        elif log_net.op == '@':
            if not log_net.op_param[0] in initializedMem:
                initializedMem.append(log_net.op_param[0])
                f.write("    cmem %s_%s : UInt<%s>[%s]\n" %
                        (log_net.op_param[1].name, log_net.op_param[0],
                         log_net.op_param[1].bitwidth, 2**log_net.op_param[1].addrwidth))
            f.write("    when %s :\n" % log_net.args[2].name)
            f.write("      infer mport T_%d  = %s_%s[%s], clock\n" %
                    (node_cntr, log_net.op_param[1].name, log_net.op_param[0],
                     log_net.args[0].name))
            f.write("      T_%d <= %s\n" % (node_cntr, log_net.args[1].name))
            f.write("      skip\n")
            node_cntr += 1
        else:
            pass

    return 0


def _trivialgraph_default_namer(thing, is_edge=True):
    """ Returns a "good" string for thing in printed graphs. """
    if is_edge:
        if thing.name is None or thing.name.startswith('tmp'):
            return ''
        else:
            return '/'.join([thing.name, str(len(thing))])
    elif isinstance(thing, Const):
        return str(thing.val)
    elif isinstance(thing, WireVector):
        return thing.name or '??'
    else:
        try:
            return thing.op + str(thing.op_param or '')
        except AttributeError:
            raise PyrtlError('no naming rule for "%s"' % str(thing))


def net_graph(block=None, split_state=False):
    """ Return a graph representation of the given block.

    :param block: block to use (defaults to current working block)
    :param split_state: if True, split connections to/from a register update net; this
        means that registers will be appear as source nodes of the network, and
        'r' nets (i.e. the logic for setting a register's next value) will
        be treated as sink nodes of the network

    Graph has the following form:
        { node1: { nodeA: edge1A, nodeB: edge1B},
          node2: { nodeB: edge2B, nodeC: edge2C},
          ...
        }

    aka: edge = graph[source][dest]

    Each node can be either a logic net or a WireVector (e.g. an Input, an Output, a
    Const or even an undriven WireVector (which acts as a source or sink in the network)
    Each edge is a WireVector or derived type (Input, Output, Register, etc.)
    Note that inputs, consts, and outputs will be both "node" and "edge".
    WireVectors that are not connected to any nets are not returned as part
    of the graph.
    """
    # FIXME: make it not try to add unused wires (issue #204)
    block = working_block(block)
    from .wire import Register
    # self.sanity_check()
    graph = {}

    # add all of the nodes
    for net in block.logic:
        graph[net] = {}

    wire_src_dict, wire_dst_dict = block.net_connections()
    dest_set = set(wire_src_dict.keys())
    arg_set = set(wire_dst_dict.keys())
    dangle_set = dest_set.symmetric_difference(arg_set)
    for w in dangle_set:
        graph[w] = {}
    if split_state:
        for w in block.wirevector_subset(Register):
            graph[w] = {}

    # add all of the edges
    for w in (dest_set | arg_set):
        try:
            _from = wire_src_dict[w]
        except Exception:
            _from = w  # e.g. an Input/Const
        if split_state and isinstance(w, Register):
            _from = w

        try:
            _to_list = wire_dst_dict[w]
        except Exception:
            _to_list = [w]  # e.g. an Output

        for _to in _to_list:
            graph[_from][_to] = w

    return graph


def output_to_trivialgraph(file, namer=_trivialgraph_default_namer, block=None, split_state=False):
    """ Walk the block and output it in trivial graph format to the open file. """
    graph = net_graph(block, split_state)
    node_index_map = {}  # map node -> index

    # print the list of nodes
    for index, node in enumerate(graph):
        print('%d %s' % (index, namer(node, is_edge=False)), file=file)
        node_index_map[node] = index

    print('#', file=file)

    # print the list of edges
    for _from in graph:
        for _to in graph[_from]:
            from_index = node_index_map[_from]
            to_index = node_index_map[_to]
            edge = graph[_from][_to]
            print('%d %d %s' % (from_index, to_index, namer(edge)), file=file)


def _default_edge_namer(edge, is_to_splitmerge=False, extra_edge_info=None):
    """
    A function for naming an edge for use in the graphviz graph.

    :param edge: the edge (i.e. WireVector or deriving class)
    :param is_to_splitmerge: if the node to which the edge points
                             is a select or concat operation
    :param extra_edge_info: a map from edge to any additional data you want
                            to print associated with it (e.g. timing data).
    :return: a function that can be called by graph namer function you pass
             in to block_to_graphviz_string
    """

    name = '' if edge.name is None else '/'.join([edge.name, str(len(edge))])
    if extra_edge_info and edge in extra_edge_info:
        # Always label an edge if present in the extra_edge_info map
        name = name + " (" + str(extra_edge_info[edge]) + ")"
    elif (edge.name is None
          or edge.name.startswith('tmp')
          or isinstance(edge, (Input, Output, Const, Register))):
        name = ''

    penwidth = 2 if len(edge) == 1 else 6
    arrowhead = 'none' if is_to_splitmerge else 'normal'
    return '[label="%s", penwidth="%d", arrowhead="%s"]' % (name, penwidth, arrowhead)


def _default_node_namer(node, split_state=False, extra_node_info=None):
    def label(v):
        if extra_node_info and node in extra_node_info:
            v = v + "(" + str(extra_node_info[node]) + ")"
        return v

    if isinstance(node, Const):
        name = node.name + ': ' if not node.name.startswith('const_') else ''
        return '[label="%s", shape=circle, fillcolor=lightgrey]' % label(name + str(node.val))
    elif isinstance(node, Input):
        return '[label="%s", shape=invhouse, fillcolor=coral]' % label(node.name)
    elif isinstance(node, Output):
        return '[label="%s", shape=house, fillcolor=lawngreen]' % label(node.name)
    elif isinstance(node, Register):
        return '[label="%s", shape=square, fillcolor=gold]' % label(node.name)
    elif isinstance(node, WireVector):
        return '[label="%s", shape=circle, fillcolor=none]' % label(node.name)
    else:
        try:
            if node.op == '&':
                return '[label="%s"]' % label("and")
            elif node.op == '|':
                return '[label="%s"]' % label("or")
            elif node.op == '^':
                return '[label="%s"]' % label("xor")
            elif node.op == '~':
                return '[label="%s", shape=invtriangle]' % label("not")
            elif node.op == 'x':
                return '[label="%s", shape=invtrapezium]' % label("mux")
            elif node.op == 's':
                selEnd = node.op_param[0]
                if len(node.op_param) < 2:
                    selBegin = selEnd
                else:
                    selBegin = node.op_param[len(node.op_param) - 1]
                return '[label="%s", height=.1, width=.1]' % \
                    (label("bits(%s,%s)" % (selBegin, selEnd)))
            elif node.op in 'c':
                return '[label="%s", height=.1, width=.1]' % label("concat")
            elif node.op == 'r':
                name = node.dests[0].name or ''
                name = ("%s.next" % name) if split_state else name
                return '[label="%s", shape=square, fillcolor=gold]' % label(name)
            elif node.op == 'w':
                return '[label="%s", height=.1, width=.1]' % label("")
            else:
                return '[label="%s"]' % label(node.op + str(node.op_param or ''))
        except AttributeError:
            raise PyrtlError('no naming rule for "%s"' % str(node))


def graphviz_default_namer(
        thing,
        is_edge,
        is_to_splitmerge,
        split_state,
        node_namer=_default_node_namer,
        edge_namer=_default_edge_namer):
    """ Returns a "good" graphviz label for thing. """
    if is_edge:
        return edge_namer(thing, is_to_splitmerge=is_to_splitmerge)
    else:
        return node_namer(thing, split_state=split_state)


def graphviz_detailed_namer(
        extra_node_info=None,
        extra_edge_info=None):
    """ Returns a detailed namer that prints extra information about nodes/edges in the given maps

        :param extra_node_info: A map from node to some object about that node
                                (its string representation will be printed next to the node's label)
        :param extra_edge_info: A map from edge to some object about that edge
                                (its string representation will be printed next to the edge's label)
        :return: a function that can be used as the namer function for block_to_graphviz_string
    """

    def node_namer(node, split_state):
        return _default_node_namer(node, split_state, extra_node_info)

    def edge_namer(edge, is_to_splitmerge):
        return _default_edge_namer(edge, is_to_splitmerge, extra_edge_info)

    def namer(thing, is_edge, is_to_splitmerge, split_state):
        return graphviz_default_namer(
            thing, is_edge, is_to_splitmerge, split_state,
            node_namer=node_namer, edge_namer=edge_namer)
    return namer


def output_to_graphviz(file, namer=graphviz_default_namer, block=None, split_state=True):
    """ Walk the block and output it in graphviz format to the open file. """
    print(block_to_graphviz_string(block, namer, split_state), file=file)


def block_to_graphviz_string(block=None, namer=graphviz_default_namer, split_state=True):
    """ Return a graphviz string for the block. """
    graph = net_graph(block, split_state)
    node_index_map = {}  # map node -> index

    rstring = """\
              digraph g {\n
              graph [splines="spline"];
              node [shape=circle, style=filled, fillcolor=lightblue1,
                    fontcolor=black, fontname=helvetica, penwidth=0,
                    fixedsize=true];
              edge [labelfloat=false, penwidth=2, color=deepskyblue, arrowsize=.5];
              """

    # print the list of nodes
    for index, node in enumerate(graph):
        label = namer(node, False, False, split_state)
        rstring += '    n%s %s;\n' % (index, label)
        node_index_map[node] = index

    # print the list of edges
    for _from in graph:
        for _to in graph[_from]:
            from_index = node_index_map[_from]
            to_index = node_index_map[_to]
            edge = graph[_from][_to]
            is_to_splitmerge = True if hasattr(_to, 'op') and _to.op in 'cs' else False
            label = namer(edge, True, is_to_splitmerge, False)
            rstring += '   n%d -> n%d %s;\n' % (from_index, to_index, label)

    rstring += '}\n'
    return rstring


def output_to_svg(file, block=None, split_state=True):
    """ Output the block as an SVG to the open file. """
    print(block_to_svg(block, split_state), file=file)


def block_to_svg(block=None, split_state=True):
    """ Return an SVG for the block. """
    try:
        from graphviz import Source
        return Source(block_to_graphviz_string(block, split_state=split_state))._repr_svg_()
    except ImportError:
        raise PyrtlError('need graphviz installed (try "pip install graphviz")')


def trace_to_html(simtrace, trace_list=None, sortkey=None):
    """ Return a HTML block showing the trace. """

    from .simulation import SimulationTrace, _trace_sort_key
    if not isinstance(simtrace, SimulationTrace):
        raise PyrtlError('first arguement must be of type SimulationTrace')

    trace = simtrace.trace
    if sortkey is None:
        sortkey = _trace_sort_key

    if trace_list is None:
        trace_list = sorted(trace, key=sortkey)

    wave_template = (
        """\
        <script type="WaveDrom">
        { signal : [
        %s
        ]}
        </script>

        """
    )

    def extract(w):
        wavelist = []
        datalist = []
        last = None
        for i, value in enumerate(trace[w]):
            if last == value:
                wavelist.append('.')
            else:
                if len(w) == 1:
                    wavelist.append(str(value))
                else:
                    wavelist.append('=')
                    datalist.append(value)
                last = value

        wavestring = ''.join(wavelist)
        datastring = ', '.join(['"%d"' % data for data in datalist])
        if len(w) == 1:
            return bool_signal_template % (w, wavestring)
        else:
            return int_signal_template % (w, wavestring, datastring)

    bool_signal_template = '{ name: "%s",  wave: "%s" },'
    int_signal_template = '{ name: "%s",  wave: "%s", data: [%s] },'
    signals = [extract(w) for w in trace_list]
    all_signals = '\n'.join(signals)
    wave = wave_template % all_signals
    # print(wave)
    return wave
