#####################################################################
# Python script to convert a post-PnR Verilog testbench 
# to a post-PnR Verilog testbench based on Caravel Wrapper
# This script will
# - Replace the FPGA instance with a Caravel wrapper instance
# - Generate wrapper input ports based on a pin assignment json file
#####################################################################

import os
from os.path import dirname, abspath, isfile
import shutil
import re
import argparse
import logging
import json

#####################################################################
# Initialize logger
#####################################################################
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG)

#####################################################################
# Parse the options
#####################################################################
parser = argparse.ArgumentParser(description='Converter for post-PnR wrapper Verilog testbench')
parser.add_argument('--post_pnr_testbench', required=True,
                    help='Specify the file path for the post-PnR Verilog testbench as input')
parser.add_argument('--pin_assignment_file', required=True,
                    help='Specify the file path to the pin assignment JSON description as input')
parser.add_argument('--wrapper_testbench', required=True,
                    help='Specify the file path for the post-PnR wrapper Verilog testbench to be outputted')
args = parser.parse_args()

#####################################################################
# Check options:
# - Input file must be valid
#   Otherwise, error out
# - Remove any output file if already exist
#   TODO: give a warning when remove files
#####################################################################
if not isfile(args.post_pnr_testbench):
  logging.error("Invalid pre-PnR testbench: " + args.post_pnr_testbench + "\nFile does not exist!\n")
  exit(1)
if not isfile(args.pin_assignment_file):
  logging.error("Invalid pin assignment file: " + args.pin_assignment_file + "\nFile does not exist!\n")
  exit(1)
if isfile(args.wrapper_testbench):
  logging.warn("Remove existing post-PnR testbench: " + args.wrapper_testbench + "!\n")
  os.remove(args.wrapper_testbench)

#####################################################################
# Parse the json file
#####################################################################
json_file = open(args.pin_assignment_file, "r")
pin_data = json.load(json_file)

#####################################################################
# TODO: This is a duplicated function from the wrapper_lines_generator.py
# Should merge them and make it shareable between scripts

# A function to parse pin range from json data
# JSON pin range format is LSB:MSB
# Return pin range format is [LSB, MSB] as a list
#####################################################################
def parse_json_pin_range(json_range) :
  pin_range_str = json_range.split(':')
  assert(2 == len(pin_range_str))
  # If the range is in decend order, we will decrease the MSB by 1
  if (int(pin_range_str[0]) > int(pin_range_str[1])) :
    return range(int(pin_range_str[0]), int(pin_range_str[1]) - 1, -1)
  # If the range is in acend order, we will increase the MSB by 1
  return range(int(pin_range_str[0]), int(pin_range_str[1]) + 1)

#####################################################################
# Write the connections between wrapper ports and existing stimuli
# to the testbench file
#####################################################################
def write_testbench_wrapper_connection(tb_file, pin_data, mode_switch_io_index):
  # Switch to the logic analyzer mode for io[25] which is reserved for mode-switch purpose
  mode_switch_line = "assign " + pin_data['caravel_gpio_input_name'] + "[" + str(mode_switch_io_index) + "] = " \
                   +  "1'b0;";
  tb_file.write("    " + mode_switch_line + "\n")
   
  for pin_info in pin_data['pins']:
    #######################################################
    # For FPGA INPUTs,
    # wrapper inputs should be driven these existing wires
    # For instance:
    #   assign wrapper_input = FPGA_INPUT;
    #
    # For FPGA OUTPUTs,
    # wrapper outputs should drive these existing wires
    # For instance:
    #   assign FPGA_OUTPUT = wrapper_output;

    # - FPGA I/O ports to Caravel GPIO 
    if (("io" == pin_info['fpga_pin_type']) \
      and (1 == len(pin_info['caravel_pin_type'])) \
      and ("gpio" == pin_info['caravel_pin_type'][0])):
      # Should have only 1 port in caravel
      assert(1 == len(pin_info['caravel_pin_type']))
      assert(1 == len(pin_info['caravel_pin_index']))
      # Get pin range
      fpga_io_pin_range = parse_json_pin_range(pin_info['fpga_pin_index'])
      caravel_io_pin_range = parse_json_pin_range(pin_info['caravel_pin_index'][0])
      assert(len(list(fpga_io_pin_range)) == len(list(caravel_io_pin_range)))
      for indices in zip(list(fpga_io_pin_range), list(caravel_io_pin_range)) : 
        # FPGA input     <- Caravel input
        curr_line = "assign " + pin_data['caravel_gpio_input_name'] + "[" + str(indices[1]) + "] = " \
                  +  pin_data['fpga_gpio_input_name'] + "[" + str(indices[0]) + "];";
        tb_file.write("    " + curr_line + "\n")
        # FPGA output    -> Caravel output
        curr_line = "assign " + pin_data['fpga_gpio_output_name'] + "[" + str(indices[0]) + "] = " \
                  +  pin_data['caravel_gpio_output_name'] + "[" + str(indices[1]) + "];";
        tb_file.write("    " + curr_line + "\n")
        # FPGA direction -> Caravel direction
        curr_line = "assign " + pin_data['fpga_gpio_direction_name'] + "[" + str(indices[0]) + "] = " \
                  +  pin_data['caravel_gpio_direction_name'] + "[" + str(indices[1]) + "];";
        tb_file.write("    " + curr_line + "\n")

    # - FPGA control input ports to Caravel GPIO 
    if (("io" != pin_info['fpga_pin_type']) \
      and (1 == len(pin_info['caravel_pin_type'])) \
      and ("input" == pin_info['caravel_pin_type'][0])):
      # Should have only 1 port in caravel
      assert(1 == len(pin_info['caravel_pin_type']))
      assert(1 == len(pin_info['caravel_pin_index']))
      # Get pin range
      fpga_io_pin_range = parse_json_pin_range(pin_info['fpga_pin_index'])
      caravel_io_pin_range = parse_json_pin_range(pin_info['caravel_pin_index'][0])
      assert(len(list(fpga_io_pin_range)) == len(list(caravel_io_pin_range)))
      for indices in zip(list(fpga_io_pin_range), list(caravel_io_pin_range)) : 
        # Connect the FPGA input port to the Caravel input
        curr_line = "assign " + pin_data['caravel_gpio_input_name'] + "[" + str(indices[1]) + "] = " \
                  +  pin_info['fpga_pin_type'] + "[" + str(indices[0]) + "];";
        tb_file.write("    " + curr_line + "\n")

    # - FPGA control output ports to Caravel GPIO 
    if (("io" != pin_info['fpga_pin_type']) \
      and (1 == len(pin_info['caravel_pin_type'])) \
      and ("output" == pin_info['caravel_pin_type'][0])):
      # Should have only 1 port in caravel
      assert(1 == len(pin_info['caravel_pin_type']))
      assert(1 == len(pin_info['caravel_pin_index']))
      # Get pin range
      fpga_io_pin_range = parse_json_pin_range(pin_info['fpga_pin_index'])
      caravel_io_pin_range = parse_json_pin_range(pin_info['caravel_pin_index'][0])
      assert(len(list(fpga_io_pin_range)) == len(list(caravel_io_pin_range)))
      for indices in zip(list(fpga_io_pin_range), list(caravel_io_pin_range)) : 
        # Tie the Caravel input to logic '0'
        curr_line = "assign " + pin_data['caravel_gpio_input_name'] + "[" + str(indices[1]) + "] = 1'b0;"
        tb_file.write("    " + curr_line + "\n")
        # Connect Caravel output port to FPGA control output
        curr_line = "assign " + pin_info['fpga_pin_type'] + "[" + str(indices[0]) + "] = " \
                  +  pin_data['caravel_gpio_output_name'] + "[" + str(indices[1]) + "];";
        tb_file.write("    " + curr_line + "\n")

    # - We always try to use the logic analyzer to connect FPGA I/O ports
    if (("io" == pin_info['fpga_pin_type']) \
      and ("logic_analyzer_io" == pin_info['caravel_pin_type'][0])):
      # Get pin range
      fpga_io_pin_range = parse_json_pin_range(pin_info['fpga_pin_index'])
      caravel_io_pin_range = parse_json_pin_range(pin_info['caravel_pin_index'][0])
      assert(len(list(fpga_io_pin_range)) == len(list(caravel_io_pin_range)))
      for indices in zip(list(fpga_io_pin_range), list(caravel_io_pin_range)) : 
        ##############################################################
        # SOC INPUT will be directly driven by logic analyzer 
        # since this I/O is going to interface logic analyzer input only
        curr_line = "assign " + pin_data['caravel_logic_analyzer_input_name'] + "[" + str(indices[1]) + "] = " \
                  + pin_data['fpga_gpio_input_name'] + "[" + str(indices[0]) + "]" + ";"
        tb_file.write("    " + curr_line + "\n")
        ##############################################################
        # SOC OUTPUT will directly drive logic analyzer 
        # since this I/O is going to interface logic analyzer output only
        curr_line = "assign " + pin_data['fpga_gpio_output_name'] + "[" + str(indices[0]) + "]" \
                  +  " = " + pin_data['caravel_logic_analyzer_output_name'] + "[" + str(indices[1]) + "];"
        tb_file.write("    " + curr_line + "\n")

#####################################################################
# Open the post-pnr Verilog testbench and start modification
#####################################################################
logging.info("Converting post-PnR testbench:"+ args.post_pnr_testbench)
logging.info("To post-PnR wrapper testbench:"+ args.wrapper_testbench)
# Create output file handler
tb_file = open(args.wrapper_testbench, "w")

#################################
# Control signals to output lines
# Skip current line: when raised, current line will not be outputted
skip_current_line = False
fpga_instance_lines = False

# Read line by line from pre-PnR testbench
with open(args.post_pnr_testbench, "r") as wp:
  template_netlist = wp.readlines()
  for line_num, curr_line in enumerate(template_netlist):
    # If the current line satisfy the following conditions
    # It should be modified and outputted to post-PnR Verilog testbenches
    # Other lines can be directly copied to post-PnR Verilog testbenches
    line2output = curr_line 
    #
    # Add post_pnr to top-level module name
    if (curr_line.startswith("module")): 
      line2output = re.sub("autocheck_top_tb;$", "wrapper_autocheck_top_tb;", curr_line)
    # Add the wires required by the wrapper
    if (curr_line == "wire [0:0] sc_tail;\n"): 
      line2output = line2output \
                  + "// ---- Wrapper I/O wires ----\n" \
                  + "// ---- Power pins ----\n" \
                  + "wire [0:0] vdda1;\n" \
                  + "wire [0:0] vdda2;\n" \
                  + "wire [0:0] vssa1;\n" \
                  + "wire [0:0] vssa2;\n" \
                  + "wire [0:0] vccd1;\n" \
                  + "wire [0:0] vccd2;\n" \
                  + "wire [0:0] vssd1;\n" \
                  + "wire [0:0] vssd2;\n" \
                  + "// ---- Wishbone pins ----\n" \
                  + "wire [0:0] wb_clk_i;\n" \
                  + "wire [0:0] wb_rst_i;\n" \
                  + "wire [0:0] wbs_stb_i;\n" \
                  + "wire [0:0] wbs_cyc_i;\n" \
                  + "wire [0:0] wbs_we_i;\n" \
                  + "wire [3:0] wbs_sel_i;\n" \
                  + "wire [31:0] wbs_dat_i;\n" \
                  + "wire [31:0] wbs_adr_i;\n" \
                  + "wire [0:0] wbs_ack_o;\n" \
                  + "wire [31:0] wbs_dat_o;\n" \
                  + "// ---- Logic analyzer pins ----\n" \
                  + "wire [127:0] la_data_in;\n" \
                  + "wire [127:0] la_data_out;\n" \
                  + "wire [127:0] la_oen;\n" \
                  + "// ---- GPIO pins ----\n" \
                  + "wire [`MPRJ_IO_PADS-1:0] io_in;\n" \
                  + "wire [`MPRJ_IO_PADS-1:0] io_out;\n" \
                  + "wire [`MPRJ_IO_PADS-1:0] io_oeb;\n" \
                  + "// ---- Analog I/O pins ----\n" \
                  + "wire [`MPRJ_IO_PADS-8:0] analog_io;\n" \
                  + "// ---- User clock pin ----\n" \
                  + "wire [0:0] user_clock2;\n"
      # TODO: This is a temporary fix for the flattened analog io port
      # SHOULD BE REMOVED ABOUT UPDATED WRAPPER
      for ipin in range(31):
        line2output += "wire [0:0] analog_io_" + str(ipin) + "_;\n"
    
    # Skip all the lines about FPGA instanciation
    if (curr_line == "\tfpga_core FPGA_DUT (\n"): 
      skip_current_line = True
      fpga_instance_lines = True

    # When FPGA instance are skipped, add the wrapper instance
    if ((True == fpga_instance_lines) and (curr_line.endswith(");\n"))): 
      skip_current_line = False
      fpga_instance_lines = False
      line2output = "\tfpga_top FPGA_DUT(\n" \
                  + "\t\t\t.vdda1(vdda1),\n" \
                  + "\t\t\t.vdda2(vdda2),\n" \
                  + "\t\t\t.vssa1(vssa1),\n" \
                  + "\t\t\t.vssa2(vssa2),\n" \
                  + "\t\t\t.vccd1(vccd1),\n" \
                  + "\t\t\t.vccd2(vccd2),\n" \
                  + "\t\t\t.vssd1(vssd1),\n" \
                  + "\t\t\t.vssd2(vssd2),\n" \
                  + "\t\t\t.wb_clk_i(wb_clk_i),\n" \
                  + "\t\t\t.wb_rst_i(wb_rst_i),\n" \
                  + "\t\t\t.wbs_stb_i(wbs_stb_i),\n" \
                  + "\t\t\t.wbs_we_i(wbs_we_i),\n" \
                  + "\t\t\t.wbs_cyc_i(wbs_cyc_i),\n" \
                  + "\t\t\t.wbs_sel_i(wbs_sel_i),\n" \
                  + "\t\t\t.wbs_dat_i(wbs_dat_i),\n" \
                  + "\t\t\t.wbs_adr_i(wbs_adr_i),\n" \
                  + "\t\t\t.wbs_ack_o(wbs_ack_o),\n" \
                  + "\t\t\t.wbs_dat_o(wbs_dat_o),\n" \
                  + "\t\t\t.la_data_in(la_data_in),\n" \
                  + "\t\t\t.la_data_out(la_data_out),\n" \
                  + "\t\t\t.la_oen(la_oen),\n" \
                  + "\t\t\t.io_in(io_in),\n" \
                  + "\t\t\t.io_out(io_out),\n" \
                  + "\t\t\t.io_oeb(io_oeb),\n" \
                  #+ "\t\t\t.analog_io(analog_io),\n" \
                  #+ "\t\t\t);\n";
      # TODO: This is a temporary fix for the flattened analog io port
      # SHOULD BE REMOVED ABOUT UPDATED WRAPPER
      for ipin in range(31):
        line2output += ".analog_io_" + str(ipin) + "_(analog_io_" + str(ipin) + "_),\n"

      line2output += "\t\t\t.user_clock2(user_clock2)\n"
      line2output += "\t\t\t);\n";
      # Wire the stimuli according to pin assignment
      write_testbench_wrapper_connection(tb_file, pin_data, 25)

    # Correct the path in signal initialization
    if (re.search(r'\$deposit\(FPGA_DUT', curr_line)):
      line2output = re.sub(r'\$deposit\(FPGA_DUT', '$deposit(FPGA_DUT.fpga_core_uut', curr_line)
    
    if (False == skip_current_line):
      tb_file.write(line2output)

tb_file.close()
logging.info("Done")