"""
Utilities for fuzzing non-routing configuration. This is the counterpart to interconnect.py
"""

import threading
import tiles
import pytrellis


def fuzz_word_setting(config, name, length, get_ncl_substs, empty_bitfile=None):
    """
    Fuzz a multi-bit setting, such as LUT initialisation

    :param config: FuzzConfig instance containing target device and tile of interest
    :param name: name of the setting to store in the database
    :param length: number of bits in the setting
    :param get_ncl_substs: a callback function, that is first called with an array of bits to create a design with that setting
    :param empty_bitfile: a path to a bit file without the parameter included, optional, which is used to determine the
    default value
    """
    prefix = "thread{}_".format(threading.get_ident())
    tile_dbs = {tile: pytrellis.get_tile_bitdata(
        pytrellis.TileLocator(config.family, config.device, tiles.type_from_fullname(tile))) for tile in
        config.tiles}
    if empty_bitfile is not None:
        none_chip = pytrellis.Bitstream.read_bit(empty_bitfile).deserialise_chip()
    else:
        none_chip = None
    baseline_bitf = config.build_design(config.ncl, get_ncl_substs([False for _ in range(length)]), prefix)
    baseline_chip = pytrellis.Bitstream.read_bit(baseline_bitf).deserialise_chip()

    wsb = {tile: pytrellis.WordSettingBits() for tile in
           config.tiles}
    is_empty = {tile: True for tile in config.tiles}
    for t in config.tiles:
        wsb[t].name = name
    for i in range(length):
        bit_bitf = config.build_design(config.ncl, get_ncl_substs([(_ == i) for _ in range(length)]), prefix)
        bit_chip = pytrellis.Bitstream.read_bit(bit_bitf).deserialise_chip()
        diff = bit_chip - baseline_chip
        for tile in config.tiles:
            if tile in diff:
                wsb[tile].bits.append(pytrellis.BitGroup(diff[tile]))
                is_empty[tile] = False
            else:
                wsb[tile].bits.append(pytrellis.BitGroup())
            if none_chip is not None:
                if wsb[tile].bits[i].match(none_chip.tiles[tile].cram):
                    wsb[tile].defval.append(True)
                else:
                    wsb[tile].defval.append(False)
    for t in config.tiles:
        if not is_empty[t]:
            tile_dbs[t].add_setting_word(wsb[t])
            tile_dbs[t].save()


def fuzz_enum_setting(config, name, values, get_ncl_substs, empty_bitfile=None, include_zeros=True, ignore_cover=None,
                      opt_pref=None, ignore_bits=None):
    """
    Fuzz a setting with multiple possible values

    :param config: FuzzConfig instance containing target device and tile of interest
    :param name: name of the setting to store in the database
    :param values: list of values taken by the enum
    :param get_ncl_substs: a callback function, that is first called with an array of bits to create a design with that setting
    :param empty_bitfile: a path to a bit file without the parameter included, optional, which is used to determine the
    default value
    :param include_zeros: if set, bits set to zero are not included in db. Needed for settings such as CEMUX which share
    bits with routing muxes to prevent conflicts.
    :param ignore_cover: these values will also be checked, and bits changing between these will be ignored
    :param opt_pref: bits exclusively set in these options will be included in all options overriding include_zeros
    """
    prefix = "thread{}_".format(threading.get_ident())
    tile_dbs = {tile: pytrellis.get_tile_bitdata(
        pytrellis.TileLocator(config.family, config.device, tiles.type_from_fullname(tile))) for tile in
        config.tiles}
    if empty_bitfile is not None:
        none_chip = pytrellis.Bitstream.read_bit(empty_bitfile).deserialise_chip()
    else:
        none_chip = None

    changed_bits = set()
    prev_tiles = {}
    tiles_changed = set()
    for val in values:
        print("****** Fuzzing {} = {} ******".format(name, val))
        bit_bitf = config.build_design(config.ncl, get_ncl_substs(val), prefix)
        bit_chip = pytrellis.Bitstream.read_bit(bit_bitf).deserialise_chip()
        for prev in prev_tiles.values():
            for tile in config.tiles:
                diff = bit_chip.tiles[tile].cram - prev[tile]
                if len(diff) > 0:
                    tiles_changed.add(tile)
                    for bit in diff:
                        changed_bits.add((tile, bit.frame, bit.bit))
        prev_tiles[val] = {}
        for tile in config.tiles:
            prev_tiles[val][tile] = bit_chip.tiles[tile].cram
    if ignore_cover is not None:
        ignore_changed_bits = set()
        ignore_prev_tiles = {}
        for ival in ignore_cover:
            print("****** Fuzzing {} = {} [to ignore] ******".format(name, ival))
            bit_bitf = config.build_design(config.ncl, get_ncl_substs(ival), prefix)
            bit_chip = pytrellis.Bitstream.read_bit(bit_bitf).deserialise_chip()
            for prev in ignore_prev_tiles.values():
                for tile in config.tiles:
                    diff = bit_chip.tiles[tile].cram - prev[tile]
                    if len(diff) > 0:
                        for bit in diff:
                            ignore_changed_bits.add((tile, bit.frame, bit.bit))
            ignore_prev_tiles[ival] = {}
            for tile in config.tiles:
                ignore_prev_tiles[ival][tile] = bit_chip.tiles[tile].cram
        for ibit in ignore_changed_bits:
            if ibit in changed_bits:
                changed_bits.remove(ibit)
    for tile in tiles_changed:
        esb = pytrellis.EnumSettingBits()
        esb.name = name
        pref_exclusive = {}
        if opt_pref is not None:
            for val in values:
                for (btile, bframe, bbit) in changed_bits:
                    if btile == tile:
                        state = prev_tiles[val][tile].bit(bframe, bbit)
                        if state:
                            if val in opt_pref:
                                if (btile, bframe, bbit) not in pref_exclusive:
                                    pref_exclusive[(btile, bframe, bbit)] = True
                            else:
                                pref_exclusive[(btile, bframe, bbit)] = False
        for val in values:
            bg = pytrellis.BitGroup()
            for (btile, bframe, bbit) in changed_bits:
                if btile == tile:
                    state = prev_tiles[val][tile].bit(bframe, bbit)
                    if state == 0 and not include_zeros and (
                            none_chip is not None and not none_chip.tiles[tile].cram.bit(bframe, bbit)) \
                            and (
                            (btile, bframe, bbit) not in pref_exclusive or not pref_exclusive[(btile, bframe, bbit)]):
                        continue
                    if ignore_bits is not None and ignore_bits.count((btile, bframe, bbit)):
                        continue
                    cb = pytrellis.ConfigBit()
                    cb.frame = bframe
                    cb.bit = bbit
                    cb.inv = (state == 0)
                    bg.bits.add(cb)
            esb.options[val] = bg
            if none_chip is not None and bg.match(none_chip.tiles[tile].cram):
                esb.defval = val
        tile_dbs[tile].add_setting_enum(esb)
        tile_dbs[tile].save()
