#!/usr/bin/env python3
"""
The generator
"""
import argparse
import simplejson as json

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

PINOUT = {
    "basys3":
        {
            "BUFG":
                "BUFGCTRL_X0Y0",
            "clock":
                "W5",
            "led":
                [
                    "U16",
                    "E19",
                    "U19",
                    "V19",
                    "W18",
                    "U15",
                    "U14",
                    "V14",
                    "V13",
                    "V3",
                    "W3",
                    "U3",
                    "P3",
                    "N3",
                    "P1",
                    "L1",
                ],
            "single-ended":
                [
                    # Basys3 JB 1-4, 7-10
                    "A14",
                    "A16",
                    "B15",
                    "B16",
                    "A15",
                    "A17",
                    "C15",
                    "C16",
                    # Basys3 JC 1-4, 7-10
                    "K17",
                    "M18",
                    "N17",
                    "P18",
                    "L17",
                    "M19",
                    "P17",
                    "R18"
                ],
            "differential":
                [
                    # Basys3 JB
                    ("A14", "A15"),
                    ("A16", "A17"),
                    ("C15", "B15"),
                    ("B16", "C16"),
                    # Basys3 JC
                    ("M19", "M18"),
                    ("K17", "L17"),
                    ("N17", "P17"),
                    ("P18", "R18"),
                ]
        },
    "arty":
        {
            "BUFG":
                "BUFGCTRL_X0Y0",
            "clock":
                "E3",
            "led":
                [
                    "G6",  # R0
                    "G3",  # R1
                    "J3",  # R2
                    "K1",  # R3
                    "F6",  # G0
                    "J4",  # G1
                    "J2",  # G2
                    "H6",  # G3
                    "E1",  # B0
                    "G4",  # B1
                    "H4",  # B2
                    "K2",  # B3
                    "H5",  # LED4
                    "J5",  # LED5
                    "T9",  # LED6
                    "T10",  # LED7
                ],
            "single-ended":
                [
                    # Pmod JB
                    "E15",
                    "E16",
                    "D15",
                    "C15",
                    "J17",
                    "J18",
                    "K15",
                    "J15",
                    # Pmod JC
                    "U12",
                    "V12",
                    "V10",
                    "V11",
                    "U14",
                    "V14",
                    "T13",
                    "U13",
                ],
            "differential":
                [
                    # Pmod JB
                    ("E15", "E16"),
                    ("D15", "C15"),
                    ("J17", "J18"),
                    ("K15", "J15"),
                    # Pmod JC
                    ("U12", "V12"),
                    ("V10", "V11"),
                    ("U14", "V14"),
                    ("T13", "U13"),
                ]
        },

    # Pinout for "bottom" routing graph of 50t, only for Basys3. These pins may
    # not correspond to actual LEDs so the design may not be suitable for testing
    # on hardware but it will pass all the checks on CI.
    "basys3-bottom":
        {
            "BUFG":
                "BUFGCTRL_X0Y0",
            "clock":
                "W5",  # Bank 34
            "led":
                [
                    "V3",  # LED9
                    "W3",  # LED10
                    "U3",  # LED11
                    "W7",  # CA
                    "W6",  # CB
                    "U8",  # CC
                    "V8",  # CD
                    "U5",  # CE
                    "V5",  # CF
                    "U7",  # CG
                ],
            "single-ended":
                [
                    # Basys3 JC 1-4, 7-10
                    "K17",
                    "M18",
                    "N17",
                    "P18",
                    "L17",
                    "M19",
                    "P17",
                    "R18",
                    "U15",  # LEDs
                    "U16",
                    "V13",
                    "V14",
                ],
            "differential":
                [
                    # Basys3 JC
                    ("M18", "M19"),
                    ("L17", "K17"),
                    ("P17", "N17"),
                    ("R18", "P18"),
                ]
        },
}


def unquote(s):
    if isinstance(s, str):
        return s.replace("\"", "")
    return s


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


def generate_output(board, iostandard, drives, slews):
    """
    Generates a design which outputs 100Hz square wave to a number of pins
    in which each one has different DRIVE+SLEW setting. The IOSTANDARD is
    common for all of them.
    """

    num_ports = len(drives) * len(slews)
    iosettings = {}

    # Header
    verilog = """
module top(
    input  wire clk,
    output wire [{}:0] out
);
""".format(num_ports - 1)

    pcf = """
set_io clk {}
""".format(PINOUT[board]["clock"])

    # 100Hz square wave generator
    verilog += """
    wire        clk_bufg;
    reg  [31:0] cnt_ps;
    reg         tick;

    (* LOC = "{}" *)
    BUFG bufg (.I(clk), .O(clk_bufg));

    initial cnt_ps <= 0;
    initial tick   <= 0;

    always @(posedge clk_bufg)
        if (cnt_ps >= (100000000 / (2*100)) - 1) begin
            cnt_ps <= 0;
            tick   <= !tick;
        end else begin
            cnt_ps <= cnt_ps + 1;
            tick   <= tick;
        end
""".format(PINOUT[board]["BUFG"])

    # Output buffers
    index = 0
    for slew in slews:
        for drive in drives:

            params = {"IOSTANDARD": "\"{}\"".format(iostandard)}

            if drive is not None and drive != "0":
                params["DRIVE"] = int(drive)

            if slew is not None:
                params["SLEW"] = "\"{}\"".format(slew)

            pin = PINOUT[board]["single-ended"][index]

            verilog += """
    OBUF # ({params}) obuf_{index} (
    .I(tick),
    .O(out[{index}])
    );
            """.format(
                params=",".join(
                    [".{}({})".format(k, v) for k, v in params.items()]
                ),
                index=index
            )

            if num_ports > 1:
                pcf += "set_io out[{}] {}\n".format(index, pin)
            else:
                pcf += "set_io out {}\n".format(pin)

            iosettings[pin] = {k: unquote(v) for k, v in params.items()}
            index += 1

    # Footer
    verilog += """
endmodule
"""

    return verilog, pcf, iosettings


def generate_input(board, iostandard, in_terms):
    """
    Generates a design with singnals from external pins go through IBUFs and
    registers to LEDs. Each IBUF has differen IN_TERM setting.
    """

    num_ports = len(in_terms)
    iosettings = {}

    # Header
    verilog = """
module top(
    input  wire clk,
    input  wire [{N}:0] inp,
    output reg  [{N}:0] led
);

    initial led <= 0;
""".format(N=num_ports - 1)

    pcf = """
set_io clk {}
""".format(PINOUT[board]["clock"])

    # BUFG
    verilog += """
    wire  clk_bufg;

    (* LOC = "{}" *)
    BUFG bufg (.I(clk), .O(clk_bufg));
""".format(PINOUT[board]["BUFG"])

    # Input buffers + registers
    index = 0
    for in_term in in_terms:

        params = {
            "IOSTANDARD": "\"{}\"".format(iostandard),
            "IN_TERM": "\"{}\"".format(in_term)
        }

        pin = PINOUT[board]["single-ended"][index]

        verilog += """
    wire inp_b[{index}];

    IBUF # ({params}) ibuf_{index} (
    .I(inp[{index}]),
    .O(inp_b[{index}])
    );

    always @(posedge clk_bufg)
        led[{index}] <= inp_b[{index}];
        """.format(
            params=",".join(
                [".{}({})".format(k, v) for k, v in params.items()]
            ),
            index=index
        )

        if num_ports > 1:
            pcf += "set_io inp[{}] {}\n".format(index, pin)
            pcf += "set_io led[{}] {}\n".format(
                index, PINOUT[board]["led"][index]
            )
        else:
            pcf += "set_io inp {}\n".format(pin)
            pcf += "set_io led {}\n".format(PINOUT[board]["led"][index])

        iosettings[pin] = {k: unquote(v) for k, v in params.items()}
        index += 1

    # Footer
    verilog += """
endmodule
"""

    return verilog, pcf, iosettings


def generate_inout(board, iostandard, drives, slews):
    """
    Generates a design with INOUT buffers. Buffers cycle through states:
    L,Z,H,Z with 100Hz frequency. During the Z state, IO pins are latched
    and their state is presented on LEDs.
    """

    num_ports = len(drives) * len(slews)
    iosettings = {}

    # Header
    verilog = """
module top(
    input  wire clk,
    inout  wire [{N}:0] ino,
    output reg  [{N}:0] led
);

    initial led <= 0;

    wire [{N}:0] ino_i;
    reg ino_o;
    reg ino_t;

""".format(N=num_ports - 1)

    pcf = """
set_io clk {}
""".format(PINOUT[board]["clock"])

    # Control signal generator, data sampler
    verilog += """
    wire        clk_bufg;
    reg  [31:0] cnt_ps;

    (* LOC = "{}" *)
    BUFG bufg (.I(clk), .O(clk_bufg));

    initial cnt_ps <= 32'd0;
    initial ino_o  <= 1'b0;
    initial ino_t  <= 1'b1;

    always @(posedge clk_bufg)
        if (cnt_ps >= (100000000 / (2*100)) - 1) begin
            cnt_ps <= 0;
            ino_t  <= !ino_t;
            if (ino_t == 1'b1)
                ino_o <= !ino_o;
        end else begin
            cnt_ps <= cnt_ps + 1;
            ino_t  <= ino_t;
            ino_o  <= ino_o;
        end

    always @(posedge clk_bufg)
        if (ino_t == 1'b1)
            led <= ino_i;
        else
            led <= led;
""".format(PINOUT[board]["BUFG"])

    # INOUT buffers
    index = 0
    for slew in slews:
        for drive in drives:

            params = {"IOSTANDARD": "\"{}\"".format(iostandard)}

            if drive is not None and drive != "0":
                params["DRIVE"] = int(drive)

            if slew is not None:
                params["SLEW"] = "\"{}\"".format(slew)

            pin = PINOUT[board]["single-ended"][index]

            verilog += """
    IOBUF # ({params}) iobuf_{index} (
    .I(ino_o),
    .O(ino_i[{index}]),
    .T(ino_t),
    .IO(ino[{index}])
    );
            """.format(
                params=",".join(
                    [".{}({})".format(k, v) for k, v in params.items()]
                ),
                index=index
            )

            if num_ports > 1:
                pcf += "set_io ino[{}] {}\n".format(index, pin)
                pcf += "set_io led[{}] {}\n".format(
                    index, PINOUT[board]["led"][index]
                )
            else:
                pcf += "set_io ino {}\n".format(pin)
                pcf += "set_io led {}\n".format(PINOUT[board]["led"][index])

            iosettings[pin] = {k: unquote(v) for k, v in params.items()}
            index += 1

    # Footer
    verilog += """
endmodule
"""

    return verilog, pcf, iosettings


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


def generate_diff_output(board, iostandard, drives, slews):
    """
    Generates a design which outputs 100Hz square wave to a number of pins
    in which each one has different DRIVE+SLEW setting. The IOSTANDARD is
    common for all of them.
    """

    num_ports = len(drives) * len(slews)
    iosettings = {}

    # Header
    verilog = """
module top(
    input  wire clk,
    output wire [{N}:0] out_p,
    output wire [{N}:0] out_n
);
""".format(N=num_ports - 1)

    pcf = """
set_io clk {}
""".format(PINOUT[board]["clock"])

    # 100Hz square wave generator
    verilog += """
    wire        clk_bufg;
    reg  [31:0] cnt_ps;
    reg         tick;

    (* LOC = "{}" *)
    BUFG bufg (.I(clk), .O(clk_bufg));

    initial cnt_ps <= 0;
    initial tick   <= 0;

    always @(posedge clk_bufg)
        if (cnt_ps >= (100000000 / (2*100)) - 1) begin
            cnt_ps <= 0;
            tick   <= !tick;
        end else begin
            cnt_ps <= cnt_ps + 1;
            tick   <= tick;
        end
""".format(PINOUT[board]["BUFG"])

    # Output buffers
    index = 0
    for slew in slews:
        for drive in drives:

            params = {"IOSTANDARD": "\"{}\"".format(iostandard)}

            if drive is not None and drive != "0":
                params["DRIVE"] = int(drive)

            if slew is not None:
                params["SLEW"] = "\"{}\"".format(slew)

            pins = PINOUT[board]["differential"][index]

            verilog += """
    OBUFDS # ({params}) obuf_{index} (
    .I(tick),
    .O(out_p[{index}]),
    .OB(out_n[{index}])
    );
            """.format(
                params=",".join(
                    [".{}({})".format(k, v) for k, v in params.items()]
                ),
                index=index
            )

            if num_ports > 1:
                pcf += "set_io out_p[{}] {}\n".format(index, pins[0])
                pcf += "set_io out_n[{}] {}\n".format(index, pins[1])
            else:
                pcf += "set_io out_p {}\n".format(pins[0])
                pcf += "set_io out_n {}\n".format(pins[1])

            iosettings[pins[0]] = {k: unquote(v) for k, v in params.items()}
            iosettings[pins[1]] = {k: unquote(v) for k, v in params.items()}
            index += 1

    # Footer
    verilog += """
endmodule
"""

    return verilog, pcf, iosettings


def generate_diff_input(board, iostandard, in_terms):
    """
    Generates a design with singnals from external pins go through IBUFs and
    registers to LEDs. Each IBUF has differen IN_TERM setting.
    """

    num_ports = len(in_terms)
    iosettings = {}

    # Header
    verilog = """
module top(
    input  wire clk,
    input  wire [{N}:0] inp_p,
    input  wire [{N}:0] inp_n,
    output reg  [{N}:0] led
);

    initial led <= 0;
""".format(N=num_ports - 1)

    pcf = """
set_io clk {}
""".format(PINOUT[board]["clock"])

    # BUFG
    verilog += """
    wire  clk_bufg;

    (* LOC = "{}" *)
    BUFG bufg (.I(clk), .O(clk_bufg));
""".format(PINOUT[board]["BUFG"])

    # Input buffers + registers
    index = 0
    for in_term in in_terms:

        params = {
            "IOSTANDARD": "\"{}\"".format(iostandard),
            "IN_TERM": "\"{}\"".format(in_term)
        }

        pins = PINOUT[board]["differential"][index]

        verilog += """
    wire inp_b[{index}];

    IBUFDS # ({params}) ibuf_{index} (
    .I(inp_p[{index}]),
    .IB(inp_n[{index}]),
    .O(inp_b[{index}])
    );

    always @(posedge clk_bufg)
        led[{index}] <= inp_b[{index}];
        """.format(
            params=",".join(
                [".{}({})".format(k, v) for k, v in params.items()]
            ),
            index=index
        )

        if num_ports > 1:
            pcf += "set_io inp_p[{}] {}\n".format(index, pins[0])
            pcf += "set_io inp_n[{}] {}\n".format(index, pins[1])
            pcf += "set_io led[{}] {}\n".format(
                index, PINOUT[board]["led"][index]
            )
        else:
            pcf += "set_io inp_p {}\n".format(pins[0])
            pcf += "set_io inp_n {}\n".format(pins[1])
            pcf += "set_io led {}\n".format(PINOUT[board]["led"][index])

        iosettings[pins[0]] = {k: unquote(v) for k, v in params.items()}
        iosettings[pins[1]] = {k: unquote(v) for k, v in params.items()}
        index += 1

    # Footer
    verilog += """
endmodule
"""

    return verilog, pcf, iosettings


def generate_diff_inout(board, iostandard, drives, slews):
    """
    Generates a design with INOUT buffers. Buffers cycle through states:
    L,Z,H,Z with 100Hz frequency. During the Z state, IO pins are latched
    and their state is presented on LEDs.
    """

    num_ports = len(drives) * len(slews)
    iosettings = {}

    # Header
    verilog = """
module top(
    input  wire clk,
    inout  wire [{N}:0] ino_p,
    inout  wire [{N}:0] ino_n,
    output reg  [{N}:0] led
);

    initial led <= 0;

    wire [{N}:0] ino_i;
    reg ino_o;
    reg ino_t;

""".format(N=num_ports - 1)

    pcf = """
set_io clk {}
""".format(PINOUT[board]["clock"])

    # Control signal generator, data sampler
    verilog += """
    wire        clk_bufg;
    reg  [31:0] cnt_ps;

    (* LOC = "{}" *)
    BUFG bufg (.I(clk), .O(clk_bufg));

    initial cnt_ps <= 32'd0;
    initial ino_o  <= 1'b0;
    initial ino_t  <= 1'b1;

    always @(posedge clk_bufg)
        if (cnt_ps >= (100000000 / (2*100)) - 1) begin
            cnt_ps <= 0;
            ino_t  <= !ino_t;
            if (ino_t == 1'b1)
                ino_o <= !ino_o;
        end else begin
            cnt_ps <= cnt_ps + 1;
            ino_t  <= ino_t;
            ino_o  <= ino_o;
        end

    always @(posedge clk_bufg)
        if (ino_t == 1'b1)
            led <= ino_i;
        else
            led <= led;
""".format(PINOUT[board]["BUFG"])

    # INOUT buffers
    index = 0
    for slew in slews:
        for drive in drives:

            params = {"IOSTANDARD": "\"{}\"".format(iostandard)}

            if drive is not None and drive != "0":
                params["DRIVE"] = int(drive)

            if slew is not None:
                params["SLEW"] = "\"{}\"".format(slew)

            pins = PINOUT[board]["differential"][index]

            verilog += """
    IOBUFDS # ({params}) iobuf_{index} (
    .I(ino_o),
    .O(ino_i[{index}]),
    .T(ino_t),
    .IO(ino_p[{index}]),
    .IOB(ino_n[{index}])
    );
            """.format(
                params=",".join(
                    [".{}({})".format(k, v) for k, v in params.items()]
                ),
                index=index
            )

            if num_ports > 1:
                pcf += "set_io ino_p[{}] {}\n".format(index, pins[0])
                pcf += "set_io ino_n[{}] {}\n".format(index, pins[1])
                pcf += "set_io led[{}] {}\n".format(
                    index, PINOUT[board]["led"][index]
                )
            else:
                pcf += "set_io ino_p {}\n".format(pins[0])
                pcf += "set_io ino_n {}\n".format(pins[1])
                pcf += "set_io led {}\n".format(PINOUT[board]["led"][index])

            iosettings[pins[0]] = {k: unquote(v) for k, v in params.items()}
            iosettings[pins[1]] = {k: unquote(v) for k, v in params.items()}
            index += 1

    # Footer
    verilog += """
endmodule
"""

    return verilog, pcf, iosettings


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


def main():

    # Parse arguments
    parser = argparse.ArgumentParser()
    parser.add_argument("--board", required=True, help="Board")
    parser.add_argument("--mode", required=True, help="Generation mode")
    parser.add_argument("--iostandard", required=True, help="IOSTANDARD")
    parser.add_argument("--drive", required=False, nargs="+", help="DRIVE(s)")
    parser.add_argument("--slew", required=False, nargs="+", help="SLEW(s)")
    parser.add_argument(
        "--in_term", required=False, nargs="+", help="IN_TERM(s)"
    )
    parser.add_argument("-o", required=True, help="Design name")

    args = parser.parse_args()

    # Generate design for output IO settings
    if args.mode == "output":
        verilog, pcf, iosettings = generate_output(
            args.board, args.iostandard, args.drive, args.slew
        )
    elif args.mode == "input":
        verilog, pcf, iosettings = generate_input(
            args.board, args.iostandard, args.in_term
        )
    elif args.mode == "inout":
        verilog, pcf, iosettings = generate_inout(
            args.board, args.iostandard, args.drive, args.slew
        )
    elif args.mode == "diff_output":
        verilog, pcf, iosettings = generate_diff_output(
            args.board, args.iostandard, args.drive, args.slew
        )
    elif args.mode == "diff_input":
        verilog, pcf, iosettings = generate_diff_input(
            args.board, args.iostandard, args.in_term
        )
    elif args.mode == "diff_inout":
        verilog, pcf, iosettings = generate_diff_inout(
            args.board, args.iostandard, args.drive, args.slew
        )
    else:
        raise RuntimeError("Unknown generation mode '{}'".format(args.mode))

    # Write verilog
    with open(args.o + ".v", "w") as fp:
        fp.write(verilog)

    # Write PCF
    with open(args.o + ".pcf", "w") as fp:
        fp.write(pcf)

    # Write iosettings
    if iosettings is not None:
        with open(args.o + ".json", "w") as fp:
            json.dump(iosettings, fp, indent=2)


if __name__ == "__main__":
    main()
