#
# Copyright 2019 Ettus Research, a National Instruments Brand
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
Tag-Length-Value (TLV) based EEPROM management code
"""

import struct
import zlib


class NamedStruct:
    """
    Helper class for unpacking values from bytes
    """

    def __init__(self, fmt, keys):
        self.struct = struct.Struct(fmt)
        self.keys = keys

        # ensure no duplicate keys
        assert len(set(keys) - set([None])) == len([x for x in keys if x is not None])
        # ensure same number of keys as elements
        assert len(self.struct.unpack(bytearray(self.size))) == len(keys)

    def unpack_from(self, buf, offset=0):
        """
        Unpack values from the struct, returning a dictionary mapping a field
        name to a value. If the field name is None, the field is not included
        in the returned dictionary.

        buf -- the buffer to unpack from
        offset -- the offset within that buffer
        """

        vals = self.struct.unpack_from(buf, offset)
        return {x[0]: x[1] for x in zip(self.keys, vals) if x[0] is not None}

    @property
    def size(self):
        """
        number of values to unpack
        """
        return self.struct.size


def tlv_eeprom_validate(eeprom, expected_magic):
    """
    Validate the contents of the EEPROM and return a tuple (header, tlv)
    Header is a dictionary of the EEPROM header (magic, size, crc)
    Tlv is the raw TLV data

    eeprom -- raw eeprom data
    expected_magic -- magic value that's expected
    """

    def crc32(data, initial=0):
        initial = initial ^ 0xFFFFFFFF
        crc = zlib.crc32(data, initial)
        return crc ^ 0xFFFFFFFF

    size_offset = 8
    tlv_eeprom_hdr = NamedStruct("< I I I", ["magic", "crc", "size"])
    hdr = tlv_eeprom_hdr.unpack_from(eeprom)

    if hdr["magic"] != expected_magic:
        raise RuntimeError(
            "Received incorrect EEPROM magic. "
            "Read: {:08X} Expected: {:08X}".format(hdr["magic"], expected_magic)
        )

    if hdr["size"] > (len(eeprom) - tlv_eeprom_hdr.size):
        raise RuntimeError("invalid size")

    crc = crc32(eeprom[size_offset : tlv_eeprom_hdr.size + hdr["size"]])
    if hdr["crc"] != crc:
        raise RuntimeError(
            "Received incorrect CRC. " "Read: {:08X} Expected: {:08X}".format(hdr["crc"], crc)
        )

    return hdr, eeprom[tlv_eeprom_hdr.size : tlv_eeprom_hdr.size + hdr["size"]]


def tlv_eeprom_unpack(tlv, tagmap):
    """
    Parse TLV data and return a dictionary of values found

    tlv -- raw TLV data from the EEPROM
    tagmap -- dictionary mapping 8-bit tag to a NamedStruct instance
    """

    values = {}
    hdr_struct = NamedStruct("< B B", ["tag", "len"])
    idx = 0

    while idx < len(tlv):
        hdr = hdr_struct.unpack_from(tlv, idx)
        idx += hdr_struct.size

        if hdr["tag"] in tagmap:
            val_struct = tagmap[hdr["tag"]]
            if hdr["len"] != val_struct.size:
                raise RuntimeError(
                    "unexpected size: {:d}, expected: {:d}".format(hdr["len"], tagmap[hdr["tag"]])
                )
            unpacked = val_struct.unpack_from(tlv, idx)
            # prohibit clobbering existing values
            assert len(set(values.keys()) & set(unpacked.keys())) == 0
            values.update(unpacked)

        idx += hdr["len"]

    return values


def read_eeprom(nvmem_path, tagmap, expected_magic, max_size=None):
    """
    Read the EEPROM located at nvmem_path and return a tuple (header, data)
    Header is a dictionary of values unpacked from eeprom based upon tagmap
    Data contains the full eeprom contents

    nvmem_path -- Path to readable file (typically something in sysfs)
    tagmap -- dictionary mapping an 8-bit tag to a NamedStruct instance
    expected_magic -- magic value that is expected
    max_size -- Max number of bytes to read from nvmem, whole file if omitted

    Tagmap should be a dictionary mapping an 8-bit tag to a NamedStruct
    instance. For each tag that's found in the eeprom, the value at that tag
    will be unpacked using the associated NamedStruct; any named fields within
    that struct will then be added to the returned dictionary.
    """

    max_size = max_size or -1
    with open(nvmem_path, "rb") as nvmem_file:
        data = nvmem_file.read(max_size)
    _, tlv = tlv_eeprom_validate(data, expected_magic)
    return tlv_eeprom_unpack(tlv, tagmap), data
