from os import path
import sys
import argparse
import xilinx_device
import filters
import struct
import parse_sdf
sys.path.append(path.join(path.dirname(__file__), "../../.."))
from himbaechel_dbgen.chip import *

def lookup_port_type(t):
    if t == "INPUT": return PinType.INPUT
    elif t == "OUTPUT": return PinType.OUTPUT
    elif t == "BIDIR": return PinType.INOUT
    else: assert False

def gen_bel_name(site, bel_name):
    prim_st = site.primary.site_type()
    if prim_st in ("IOB33M", "IOB33S", "IOB33", "IOB18M", "IOB18S", "IOB18"):
        return f"{site.site_type()}.{bel_name}"
    else:
        return f"{bel_name}"

@dataclass
class PipClass(Enum):
    TILE_ROUTING = 0
    SITE_ENTRANCE = 1
    SITE_EXIT = 2
    SITE_INTERNAL = 3
    LUT_PERMUTATION = 4
    LUT_ROUTETHRU = 5
    CONST_DRIVER = 6

@dataclass
class PipExtraData(BBAStruct):
    site_key: int = -1
    bel_name: IdString = field(default_factory=IdString)
    pip_config: int = 0

    def serialise_lists(self, context: str, bba: BBAWriter):
        pass
    def serialise(self, context: str, bba: BBAWriter):
        bba.u32(self.site_key)
        bba.u32(self.bel_name.index)
        bba.u32(self.pip_config)

@dataclass
class BelExtraData(BBAStruct):
    name_in_site: IdString

    def serialise_lists(self, context: str, bba: BBAWriter):
        pass
    def serialise(self, context: str, bba: BBAWriter):
        bba.u32(self.name_in_site.index)

@dataclass
class SiteInst(BBAStruct):
    name_prefix: IdString
    site_x: int
    site_y: int
    rel_x: int
    rel_y: int
    int_x: int
    int_y: int
    variants: list[IdString] = field(default_factory=list)

    def serialise_lists(self, context: str, bba: BBAWriter):
        bba.label(f"{context}_variants")
        for i, variant in enumerate(self.variants):
            bba.u32(variant.index)
    def serialise(self, context: str, bba: BBAWriter):
        bba.u32(self.name_prefix.index)
        bba.u16(self.site_x)
        bba.u16(self.site_y)
        bba.u16(self.rel_x)
        bba.u16(self.rel_y)
        bba.u16(self.int_x)
        bba.u16(self.int_y)
        bba.slice(f"{context}_variants", len(self.variants))

@dataclass
class TileExtraData(BBAStruct):
    name_prefix: IdString
    tile_x: int
    tile_y: int
    sites: list[SiteInst] = field(default_factory=list)

    def serialise_lists(self, context: str, bba: BBAWriter):
        for i, site in enumerate(self.sites):
            site.serialise_lists(f"{context}_si{i}", bba)
        bba.label(f"{context}_sites")
        for i, site in enumerate(self.sites):
            site.serialise(f"{context}_si{i}", bba)
    def serialise(self, context: str, bba: BBAWriter):
        bba.u32(self.name_prefix.index)
        bba.u16(self.tile_x)
        bba.u16(self.tile_y)
        bba.slice(f"{context}_sites", len(self.sites))

def timing_pip_classs(pip: xilinx_device.PIP):
    return f"{'buf' if pip.is_buffered() else 'sw'}_d{pip.max_delay()}_r{pip.resistance()}_c{pip.capacitance()}"

seen_pip_timings = set()
seen_node_timings = set()

def import_tiletype(ch: Chip, tile: xilinx_device.Tile):
    tile_type = tile.tile_type()
    if tile.x == 0 and tile.y == 0:
        assert tile_type == "NULL"
        tile_type = "NULL_CORNER"
    tt = ch.create_tile_type(tile_type)
    # import tile wires
    for wire in tile.wires():
        nw = tt.create_wire(wire.name(), wire.intent())
        nw.flags = -1 # not a site wire
        if wire.is_gnd():
            nw.const_value = ch.strs.id("GND")
        if wire.is_vcc():
            nw.const_value = ch.strs.id("VCC")
    if tile_type == "NULL_CORNER":
        # pseudo ground bels
        tt.create_wire(f"GND", "GND", const_value="GND")
        tt.create_wire(f"VCC", "VCC", const_value="VCC")

        gnd = tt.create_bel(f"PSEUDO_GND", f"PSEUDO_GND", z=0)
        gnd.site = -1
        gnd.extra_data = BelExtraData(name_in_site=ch.strs.id("PSEUDO_GND"))
        tt.add_bel_pin(gnd, "Y", "GND", PinType.OUTPUT)

        vcc = tt.create_bel(f"PSEUDO_VCC", f"PSEUDO_VCC", z=1)
        vcc.site = -1
        vcc.extra_data = BelExtraData(name_in_site=ch.strs.id("PSEUDO_VCC"))
        tt.add_bel_pin(vcc, "Y", "VCC", PinType.OUTPUT)
    def add_pip(src_wire, dst_wire, pip_class=PipClass.TILE_ROUTING, timing="",
        site_key=-1, bel_name="", pip_config=0):
        np = tt.create_pip(src_wire, dst_wire, timing)
        np.flags = pip_class.value
        np.extra_data = PipExtraData(site_key=site_key, bel_name=ch.strs.id(bel_name),
            pip_config=pip_config)

    def lookup_site_wire(sw):
        canon_name = f"{sw.site.rel_name()}.{sw.name()}"
        if not tt.has_wire(canon_name):
            nw = tt.create_wire(name=canon_name,
                type="INTENT_SITE_GND" if sw.name() == "GND_WIRE" else "INTENT_SITE_WIRE")
            nw.flags = sw.site.primary.index
        return canon_name

    def add_site_io_pip(pin):
        pn = pin.name()
        s = pin.site
        site_key = (s.index << 8)
        if pin.tile_wire() is None:
            return None
        # TODO: timing class
        if pin.dir() in ("OUTPUT", "BIDIR"):
            if s.primary.site_type() == "IPAD" and pn == "O":
                return None
            # if s.rel_xy()[0] == 0 and s.primary.site_type() == "SLICEL" and pin.site_wire().name() == "A":
            #     # Add ground pip
            #     # self.add_pseudo_pip(self.row_gnd_wire_index, self.sitewire_to_tilewire(pin.site_wire()),
            #     #    pip_type=NextpnrPipType.CONST_DRIVER)
            #     pass
            add_pip(lookup_site_wire(pin.site_wire()), pin.tile_wire().name(),
                pip_class=PipClass.SITE_EXIT, timing="SITE_NULL")
        else:
            if s.site_type() in ("SLICEL", "SLICEM"):
                # Add permuation pseudo-pips for LUT inputs
                swn = pin.site_wire().name()
                if len(swn) == 2 and swn[0] in "ABCDEFGH" and swn[1] in "123456":
                    i = int(swn[1])
                    for j in range(1, 7):
                        if (i == 6) != (j == 6):
                            continue # don't allow permutation of input 6
                        pip_config = ("ABCDEFGH".index(swn[0]) << 8) | ((j - 1) << 4) | (i - 1)
                        if s.rel_xy()[0] == 1:
                            pip_config |= (4 << 8)
                        add_pip(s.pin(f"{swn[0]}{j}").tile_wire().name(), lookup_site_wire(pin.site_wire()),
                            pip_class=PipClass.LUT_PERMUTATION,
                            pip_config=pip_config,
                            site_key=(s.index << 8),
                            timing="SITE_NULL"
                        )
                    return
            add_pip(pin.tile_wire().name(), lookup_site_wire(pin.site_wire()),
                pip_class=PipClass.SITE_ENTRANCE, timing="SITE_NULL")

    # TODO: ground/vcc
    tile_wire_count = len(tt.wires)
    for site in tile.sites():
        seen_pins = set()
        for variant_idx, variant in enumerate(site.available_variants()):
            if variant in ("FIFO36E1", ): #unsupported atm
                continue
            sv = site.variant(variant)
            variant_key = (site.index << 8) | (variant_idx & 0xFF)
            # Import site bels
            for bel in sv.bels():
                z = filters.get_bel_z_override(bel, len(tt.bels))
                # Overriden z of -1 means we skip this bel
                if z == -1:
                    continue
                bel_name = gen_bel_name(sv, bel.name())
                nb = tt.create_bel(name=f"{site.rel_name()}.{bel_name}", type=filters.get_bel_type_override(bel.bel_type()), z=z)
                nb.site = variant_key
                nb.extra_data = BelExtraData(name_in_site=ch.strs.id(bel_name))
                if bel.bel_class() == "RBEL": nb.flags |= 2
                # TODO: extra data (site etc)
                for pin in bel.pins():
                    tt.add_bel_pin(nb, pin.name, lookup_site_wire(pin.site_wire()), lookup_port_type(pin.dir()))
            # Import site pins
            for pin in sv.pins():
                pin_key = (pin.name(), pin.site_wire().name())
                if pin_key not in seen_pins:
                    add_site_io_pip(pin)
                    seen_pins.add(pin_key)
            # Import site pips
            for site_pip in sv.pips():
                if "LUT" in site_pip.bel().bel_type():
                    continue # ignore site LUT route-throughs
                bel_name = site_pip.bel().name()
                bel_pin = site_pip.bel_input()
                if (bel_name == "ADI1MUX" and bel_pin == "BDI1") or \
                    (bel_name == "BDI1MUX" and bel_pin == "DI") or \
                    (bel_name == "CDI1MUX" and bel_pin == "DI") or \
                    (bel_name.startswith("TFBUSED")) or \
                    (bel_name == "OMUX"):
                    continue
                add_pip(lookup_site_wire(site_pip.src_wire()),
                    lookup_site_wire(site_pip.dst_wire()),
                    pip_class=PipClass.SITE_INTERNAL,
                    site_key=variant_key,
                    bel_name=bel_name,
                    pip_config=ch.strs.id(bel_pin).index,
                    timing="SITE_NULL"
                )
    # Import tile pips
    for pip in tile.pips():
        if not filters.include_pip(tile.tile_type(), pip):
            continue
        tcls = timing_pip_classs(pip)
        if tcls not in seen_pip_timings:
            ch.timing.set_pip_class(f"DEFAULT", tcls,
                delay=TimingValue(int(pip.min_delay()*1000), int(pip.max_delay()*1000)), # ps
                in_cap=TimingValue(int(pip.capacitance()*1000)), # fF
                out_res=TimingValue(int(pip.resistance())), # mohm
                is_buffered=pip.is_buffered())
            seen_pip_timings.add(tcls)
        add_pip(pip.src_wire().name(), pip.dst_wire().name(), pip_class=PipClass.TILE_ROUTING, timing=tcls,
            pip_config=1 if pip.is_route_thru() else 0)
        # TODO: extra data, route-through flag
        if pip.is_bidi():
            add_pip(pip.dst_wire().name(), pip.src_wire().name(), pip_class=PipClass.TILE_ROUTING, timing=tcls,
                pip_config=1 if pip.is_route_thru() else 0)

def import_sdf_timings(variant, sdfcell):
    # TODO: anything other than comb
    for entry in sdfcell.entries:
        if isinstance(entry, parse_sdf.IOPath):
            variant.add_comb_arc(entry.from_pin, entry.to_pin,
                TimingValue(int(min(entry.rising.minv, entry.falling.minv)*1000),
                    int(max(entry.rising.maxv, entry.falling.maxv)*1000)))

def main():
    xlbase = path.join(path.dirname(path.realpath(__file__)), "..")

    parser = argparse.ArgumentParser()
    parser.add_argument("--xray", help="Project X-Ray device database path for current family (e.g. ../prjxray-db/artix7)", type=str, required=True)
    parser.add_argument("--metadata", help="nextpnr-xilinx site metadata root", type=str, default=path.join(xlbase, "meta", "artix7"))
    parser.add_argument("--device", help="name of device to export", type=str, required=True)
    parser.add_argument("--constids", help="name of nextpnr constids file to read", type=str, default=path.join(xlbase, "constids.inc"))
    parser.add_argument("--bba", help="bba file to write", type=str, required=True)
    args = parser.parse_args()

    # Init database paths
    metadata_root = args.metadata
    xraydb_root = args.xray
    if "xc7z" in args.device:
        metadata_root = metadata_root.replace("artix7", "zynq7")
        xraydb_root = xraydb_root.replace("artix7", "zynq7")
    if "xc7k" in args.device:
        metadata_root = metadata_root.replace("artix7", "kintex7")
        xraydb_root = xraydb_root.replace("artix7", "kintex7")
    if "xc7s" in args.device:
        metadata_root = metadata_root.replace("artix7", "spartan7")
        xraydb_root = xraydb_root.replace("artix7", "spartan7")
    # Load prjxray device data
    d = xilinx_device.import_device(args.device, xraydb_root, metadata_root)
    # Init constant ids
    ch = Chip("xilinx", args.device, d.width, d.height)
    ch.strs.read_constids(path.join(path.dirname(__file__), args.constids))
    ch.set_speed_grades(["DEFAULT", ]) # TODO: figure out how speed grades are supposed to work in prjxray
    # Import tile types
    for tile in d.tiles:
        tile_type = tile.tile_type()
        if tile.x == 0 and tile.y == 0:
            assert tile_type == "NULL"
            # location for pseudo gnd/vcc bels
            tile_type = "NULL_CORNER"
        if tile_type not in ch.tile_type_idx:
            import_tiletype(ch, tile)
        ti = ch.set_tile_type(tile.x, tile.y, tile_type)
        prefix, tx, ty = tile.split_name()
        ti.extra_data = TileExtraData(ch.strs.id(prefix), tx, ty)
        for site in tile.sites():
            ti.extra_data.sites.append(SiteInst(
                name_prefix=ch.strs.id(site.prefix),
                site_x=site.grid_xy[0], site_y=site.grid_xy[1],
                rel_x=site.rel_xy()[0], rel_y=site.rel_xy()[1],
                int_x=tile.interconn_xy[0], int_y=tile.interconn_xy[1],
                variants=[ch.strs.id(v) for v in site.available_variants()]
            ))
    # Import nodes
    seen_nodes = set()
    tt_used_wires = {}
    print("Processing nodes...")
    for row in range(d.height):
        # TODO: GND and VCC row connectivity...
        for col in range(d.width):
            t = d.tiles_by_xy[col, row]
            for w in t.wires():
                n = w.node()
                uid = n.unique_index()
                if uid in seen_nodes:
                    continue
                if len(n.wires) > 1:
                    node_wires = []
                    node_cap = 0
                    node_res = 0
                    # Add interconnect tiles first for better delay estimates in nextpnr
                    for j in range(2):
                        for w in n.wires:
                            if (w.tile.tile_type() in ("INT", "INT_L", "INT_R")) != (j == 0):
                                continue
                            if w.tile.tile_type() not in tt_used_wires:
                                tt_used_wires[w.tile.tile_type()] = w.tile.used_wire_indices()
                            node_cap += int(w.capacitance() * 1000)
                            node_res += int(w.resistance())
                            # prune wires from nodes that just pass through a tile without connecting to
                            # bels/pips, because nextpnr doesn't care about these
                            if w.index not in tt_used_wires[w.tile.tile_type()]:
                                continue
                            node_wires.append(NodeWire(w.tile.x, w.tile.y, w.index))
                    if len(node_wires) > 1:
                        timing_class = f"node_c{node_cap}_c{node_res}"
                        ch.add_node(node_wires, timing_class=timing_class)
                        if timing_class not in seen_node_timings:
                            ch.timing.set_node_class("DEFAULT", timing_class, delay=TimingValue(0), cap=TimingValue(node_cap), res=TimingValue(node_res))
                            seen_node_timings.add(timing_class)
                    del node_wires
                seen_nodes.add(uid)
    # Stub timing class
    ch.timing.set_pip_class("DEFAULT", "SITE_NULL", delay=TimingValue(20))
    # Stub bel timings
    lut = ch.timing.add_cell_variant("DEFAULT", "SLICE_LUTX")
    for i in range(1, 6+1):
        lut.add_comb_arc(f"A{i}", "O6", TimingValue(100, 125))
        if i <= 5: lut.add_comb_arc(f"A{i}", "O5", TimingValue(120, 150))
    for i in range(1, 8+1):
        lut.add_setup_hold("CLK", f"WA{i}", ClockEdge.RISING, TimingValue(100, 500), TimingValue(100, 200))
    lut.add_setup_hold("CLK", "WE", ClockEdge.RISING, TimingValue(100, 600), TimingValue(100, 100))
    lut.add_setup_hold("CLK", "DI1", ClockEdge.RISING, TimingValue(100, 100), TimingValue(100, 100))
    ff = ch.timing.add_cell_variant("DEFAULT", "SLICE_FFX")
    ff.add_setup_hold("CK", "CE", ClockEdge.RISING, TimingValue(100, 100), TimingValue(0, 0))
    ff.add_setup_hold("CK", "SR", ClockEdge.RISING, TimingValue(100, 100), TimingValue(0, 0))
    ff.add_setup_hold("CK", "D", ClockEdge.RISING, TimingValue(100, 100), TimingValue(200, 200))
    ff.add_clock_out("CK", "Q", ClockEdge.RISING, TimingValue(300, 350))

    # Load SDF for carry and mux
    slicem_sdf = parse_sdf.parse_sdf_file(path.join(xraydb_root, "timings", "slicem.sdf"))
    mux = ch.timing.add_cell_variant("DEFAULT", "SELMUX2_1")
    import_sdf_timings(mux, slicem_sdf.cells[("SELMUX2_1", "SLICEM/F7BMUX")])
    carry = ch.timing.add_cell_variant("DEFAULT", "CARRY4")
    import_sdf_timings(carry, slicem_sdf.cells[("CARRY4", "SLICEM")])

    # Import package pins
    for package_name, package in sorted(d.packages.items(), key=lambda x:x[0]):
        pkg = ch.create_package(package_name)
        for pin, site in sorted(package.pin_map.items(), key=lambda x:x[0]):
            site_data = d.sites_by_name[site]
            bel_name = gen_bel_name(site_data, "PAD")
            tile_type = ch.tile_type_at(site_data.tile.x, site_data.tile.y)
            pkg.create_pad(pin, f"X{site_data.tile.x}Y{site_data.tile.y}",
            f"{site_data.rel_name()}.{bel_name}",
            "", 0) # TODO: bank
    ch.write_bba(args.bba)

if __name__ == '__main__':
    main()
