#!/usr/bin/env python3
"""
This script provides a temporary solution for the problem of inout top level
port representation of the BLIF format.

The script loads the design from a JSON file generated by Yosys. Then it splits
all inout ports along with their nets and connections to cell ports into two.
Suffixes are automatically added to distinguish between the input and the
output part.

The JSON design format used by Yosys is documented there:
- http://www.clifford.at/yosys/cmd_write_json.html
- http://www.clifford.at/yosys/cmd_read_json.html

For example in the given design (in verilog):

 module top(
  input  A,
  output B,
  inout  C
 );

  IOBUF iobuf (
   .I(A),
   .O(B),
   .IO_$inp(C),
   .IO_$out(C)
  );

 endmodule

the resulting design would be:

 module top(
  input  A,
  output B,
  input  C_$inp,
  output C_$out
 );

  IOBUF iobuf (
   .I(A),
   .O(B),
   .IO_$inp(C_$inp),
   .IO_$out(C_$out)
  );

 endmodule

"""
import argparse
import os
import simplejson as json

# =============================================================================


def find_top_module(design):
    """
    Looks for the top-level module in the design. Returns its name. Throws
    an exception if none was found.
    """

    for name, module in design["modules"].items():
        attrs = module["attributes"]
        if "top" in attrs and int(attrs["top"]) == 1:
            return name

    raise RuntimeError("No top-level module found in the design!")


def get_nets(bits):
    """
    Returns a set of numbers corresponding to net indices effectively skipping
    connections to consts ("0", "1", "x").

    >>> get_nets([0, 1, 2, "0", "1", "x", 3, 4, 5])
    {0, 1, 2, 3, 4, 5}
    """
    return set([n for n in bits if isinstance(n, int)])


def get_free_net(nets):
    """
    Given a set of used net indices, returns a new, free index.

    >>> get_free_net({0, 1, 2,    4, 5, 6})
    3
    >>> get_free_net({0, 1, 2, 3, 4, 5, 6})
    7
    """
    sorted_nets = sorted(list(nets))

    # Find a gap in the sequence
    for i in range(len(nets) - 1):
        n0 = sorted_nets[i]
        n1 = sorted_nets[i + 1]
        if n1 != (n0 + 1):
            return n0 + 1

    # No gap was found, return max + 1.
    return sorted_nets[-1] + 1


# =============================================================================


def find_and_split_inout_ports_and_nets(design, module_name):
    """
    This function takes a given module from the design and splits all of its
    inout ports into pairs of inputs and outputs. Newly created ports are
    given suffixed. For example an inout port named "A" is going to be replaced
    by a pair consisting of "A_$inp" and "A_$out" ports.

    The function also looks for "netnames" that correspond to inout ports being
    split. These ones are removed and replaced with new ones related to newly
    added input and output ports.

    If any other "netname" mentions a net index connected to a former inout
    port, then the index is removed from the "netname" (replaced by "x"). If
    there are only "x" left in the "netname", then it is removed.

    The function returns port name map and net index map. The port map is a
    list of pairs (input name, input/output name). There are two entries per
    an inout. The net map is a dict indexed by indices of nets associated with
    inout ports. Each item contains a dict like {"i": int, "o": int} with
    indices of the inout net split products.
    """

    # Get the module
    module = design["modules"][module_name]

    # Find indices of all used nets
    nets = set()
    for port in module["ports"].values():
        nets |= get_nets(port["bits"])

    for netname in module["netnames"].values():
        nets |= get_nets(netname["bits"])

    for cell in module["cells"].values():
        for connection in cell["connections"].values():
            nets |= get_nets(connection)

    # Get all inout ports
    inouts = {
        k: v
        for k, v in module["ports"].items()
        if v["direction"] == "inout"
    }

    # Split ports
    new_ports = {}
    net_map = {}
    port_map = []
    for name, port in inouts.items():

        # Remove the inout port from the module
        del module["ports"][name]
        nets -= get_nets(port["bits"])

        # Make an input and output port
        for dir in ["input", "output"]:
            new_name = name + "_$" + dir[:3]
            new_port = {"direction": dir, "bits": []}

            print("Mapping port '{}' to '{}'".format(name, new_name))

            for n in port["bits"]:
                if isinstance(n, int):
                    mapped_n = get_free_net(nets)
                    print("Mapping net {} to {} ({})".format(n, mapped_n, dir))

                    if n not in net_map:
                        net_map[n] = {}
                    net_map[n][dir[0]] = mapped_n
                    nets.add(mapped_n)

                    new_port["bits"].append(mapped_n)
                else:
                    new_port["bits"].append(n)

            port_map.append((
                name,
                new_name,
            ))
            new_ports[new_name] = new_port

    # Add inputs and outputs
    module["ports"].update(new_ports)

    # .....................................................
    netnames = module["netnames"]

    # Remove netnames related to inout ports
    for name, net in list(module["netnames"].items()):
        if name in inouts:
            print("Removing netname '{}'".format(name))
            del netnames[name]

    # Remove remapped nets
    for name, net in list(netnames.items()):

        # Remove "bits" used by the net that were re-mapped.
        if len(set(net["bits"]) & set(net_map.keys())):

            # Remove
            net["bits"] = ["x" if b in net_map else b for b in net["bits"]]

            # If there is nothing left, remove the whole net.
            if all([b == "x" for b in net["bits"]]):
                print("Removing netname '{}'".format(name))
                del netnames[name]

    # Add netnames related to new input and output ports
    for name, port in new_ports.items():
        netnames[name] = {
            "hide_name": 0,
            "bits": port["bits"],
            "attributes": {}
        }

    return port_map, net_map


def remap_connections(design, module_name, net_map):
    """
    This function remaps cell connections that mention inout ports being split.

    The function loops over all cells and their ports. If a port contains
    a connection to an inout net then the connection is remapped according to
    the given net_map. Only ports which names ends on "_$inp" and "_$out" are
    affected.
    """

    module = design["modules"][module_name]
    cells = module["cells"]

    # Process cells
    for name, cell in cells.items():
        if "port_directions" not in cell:
            continue

        port_directions = cell["port_directions"]
        connections = cell["connections"]

        # Process cell connections
        for port_name, port_nets in list(connections.items()):

            # Skip if no net of this connection were remapped
            if len(set(net_map.keys()) & set(port_nets)) == 0:
                continue

            # Remove connections to the output net from input port and vice
            # versa.
            for dir in ["input", "output"]:
                if port_directions[port_name] == dir and \
                   port_name.endswith("$" + dir[:3]):

                    for i, n in enumerate(port_nets):
                        if n in net_map:
                            mapped_n = net_map[n][dir[0]]
                            port_nets[i] = mapped_n
                            print(
                                "Mapping connection {}.{}[{}] from {} to {}".
                                format(name, port_name, i, n, mapped_n)
                            )


# =============================================================================


def main():
    """
    The main.
    """

    # Parse args
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    parser.add_argument("-i", required=True, type=str, help="Input JSON")
    parser.add_argument("-o", default=None, type=str, help="Output JSON")

    args = parser.parse_args()

    # Output name
    if args.o is None:
        args.o = os.path.splitext(args.i)[0] + "_out.json"

    # Read the design
    with open(args.i, "r") as fp:
        design = json.load(fp)

    # Find the top module
    top_name = find_top_module(design)

    # Find and split inouot ports
    port_map, net_map = find_and_split_inout_ports_and_nets(design, top_name)
    # Remap cell connections
    remap_connections(design, top_name, net_map)

    # Write the design
    with open(args.o, "w") as fp:
        json.dump(design, fp, sort_keys=True, indent=2)


if __name__ == "__main__":
    main()
