import collections.abc
import numbers
import os
import random
import struct
import sys
from typing import (
    Any,
    Callable,
    Dict,
    List,
    Optional,
    Sequence,
    Tuple,
    TypeVar,
    Union,
    cast,
)

import flatbuffers
import numpy as np
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

# Generated by `flatc`
sys.path.append(os.path.dirname(__file__))
import FlatBuffers.Dl.Attribute as Attribute
import FlatBuffers.Dl.AttributeF as AttributeF
import FlatBuffers.Dl.AttributeI as AttributeI
import FlatBuffers.Dl.AttributeType as AttributeType
import FlatBuffers.Dl.Dimension as Dimension
import FlatBuffers.Dl.DimensionValue as DimensionValue
import FlatBuffers.Dl.DimensionValueType as DimensionValueType
import FlatBuffers.Dl.Function as Function
import FlatBuffers.Dl.Graph as Graph
import FlatBuffers.Dl.Model as Model
import FlatBuffers.Dl.Node as Node
import FlatBuffers.Dl.OperatorSetId as OperatorSetId
import FlatBuffers.Dl.Tensor as Tensor
import FlatBuffers.Dl.TensorDataType as TensorDataType
import FlatBuffers.Dl.TensorShape as TensorShape
import FlatBuffers.Dl.TensorTypeAndShape as TensorTypeAndShape
import FlatBuffers.Dl.TypeInfo as TypeInfo
import FlatBuffers.Dl.TypeInfoValue as TypeInfoValue
import FlatBuffers.Dl.ValueInfo as ValueInfo
import mapping as mapping


def make_node(
    op_type: str,
    inputs: Sequence[str],
    outputs: Sequence[str],
    name: Optional[str] = None,
    doc_string: Optional[str] = None,
    domain: Optional[str] = None,
    **kwargs: Any,
) -> Node.NodeT:
    """Construct a Node.

    Args:
        op_type (string): The name of the operator to construct
        inputs (list of string): list of input names
        outputs (list of string): list of output names
        name (string, default None): optional unique identifier for NodeFbs
        doc_string (string, default None): optional documentation string for NodeFbs
        domain (string, default None): optional domain for NodeFbs.
            If it's None, we will just use default domain (which is empty)
        **kwargs (dict): the attributes of the node.  The acceptable values
            are documented in :func:`make_attribute`.

    Returns:
        Node
    """
    node = Node.NodeT()
    node.opType = op_type
    node.input = [*inputs]
    node.output = [*outputs]
    if name:
        node.name = name
    if doc_string:
        node.docString = doc_string
    if domain is not None:
        node.domain = domain
    if kwargs:
        node.attribute = [
            make_attribute(key, value)
            for key, value in sorted(kwargs.items())
            if value is not None
        ]
    return node


def make_operatorsetid(
    domain: str,
    version: int,
) -> OperatorSetId.OperatorSetIdT:
    """Construct an OperatorSetId.

    Args:
        domain (string): The domain of the operator set id
        version (integer): Version of operator set id
    Returns:
        OperatorSetId.OperatorSetIdT
    """
    operatorsetid = OperatorSetId.OperatorSetIdT()
    operatorsetid.domain = domain
    operatorsetid.version = version
    return operatorsetid


def make_graph_test_value(
    valuesForTest: Dict[str, Dict[str, np.ndarray]] = None, 
    exponents: Dict[str, List[int]] = None
) -> Tuple[Sequence[Tensor.TensorT], Sequence[Tensor.TensorT]]:
    """Construct the test_inputs_value, test_outputs_value of Graph

    Args:
        valuesForTest (Dict[str, Dict[str, np.ndarray]]): the test values used to compare accuracy.
                                                          The input format is as follows:
            {
                'inputs': {
                    'input_0_name': np.ndarray
                    ......
                    'input_n_name': np.ndarray
                },
                'outputs': {
                    'output_0_name': np.ndarray
                    ......
                    'output_n_name': np.ndarray
                },
            }

    Returns:
        Tuple[Sequence[Tensor.TensorT], Sequence[Tensor.TensorT]]
    """
    input_test_values = []
    output_test_values = []
    if valuesForTest is not None:
        # Get the input test value
        for input_name, input_value in valuesForTest.get('inputs', {}).items():
            var_exponents = exponents.get(input_name, [0])
            tensor_type = np_dtype_to_tensor_dtype(input_value.dtype)
            input_shape = input_value.shape
            if len(input_shape) > 1:
                input_value = input_value.flatten()
            if len(input_shape) == 0:
                input_shape = [1]
            input_value_fbs = make_tensor(input_name, tensor_type, input_shape, input_value.tobytes(), raw = True, exponents = var_exponents)
            input_test_values.append(input_value_fbs)

        # Get the output test value
        for output_name, output_value in valuesForTest.get('outputs', {}).items():
            var_exponents = exponents.get(output_name, [0])
            tensor_type = np_dtype_to_tensor_dtype(output_value.dtype)
            output_shape = output_value.shape
            if len(output_shape) > 1:
                output_value = output_value.flatten()
            if len(output_shape) == 0:
                output_shape = [1]
            output_value_fbs = make_tensor(output_name, tensor_type, output_shape, output_value.tobytes(), raw = True, exponents = var_exponents)
            output_test_values.append(output_value_fbs)

    return input_test_values, output_test_values


def make_graph(
    nodes: Sequence[Node.NodeT],
    name: str,
    inputs: Sequence[ValueInfo.ValueInfoT],
    outputs: Sequence[ValueInfo.ValueInfoT],
    initializer: Optional[Sequence[Tensor.TensorT]] = None,
    doc_string: Optional[str] = None,
    value_info: Optional[Sequence[ValueInfo.ValueInfoT]] = None,
    test_inputs_value: Optional[Sequence[Tensor.TensorT]] = None,
    test_outputs_value: Optional[Sequence[Tensor.TensorT]] = None,
) -> Graph.GraphT:
    """Construct a Graph

    Args:
        nodes: list of Node.NodeT
        name (string): graph name
        inputs: list of ValueInfo.ValueInfoT
        outputs: list of ValueInfo.ValueInfoT
        initializer: list of Tensor.TensorT
        doc_string (string): graph documentation
        value_info: list of ValueInfo.ValueInfoT
    Returns:
        Graph.GraphT
    """
    if initializer is None:
        initializer = []
    if value_info is None:
        value_info = []
    if test_inputs_value is None:
        test_inputs_value = []
    if test_outputs_value is None:
        test_outputs_value = []
    graph = Graph.GraphT()
    graph.node = [*nodes]
    graph.name = name
    graph.input = [*inputs]
    graph.output = [*outputs]
    graph.initializer = [*initializer]
    graph.valueInfo = [*value_info]
    if doc_string:
        graph.docString = doc_string
    graph.testInputsValue = [*test_inputs_value]
    graph.testOutputsValue = [*test_outputs_value]
    return graph


def make_model(graph: Graph.GraphT, **kwargs: Any) -> Model.ModelT:
    """Construct a Model

    Args:
        graph (Graph.GraphT): *make_graph* returns
        **kwargs: any attribute to add to the returned instance
    Returns:
        Model.ModelT
    """
    model = Model.ModelT()
    model.graph = graph

    opset_imports: Optional[Sequence[OperatorSetId.OperatorSetIdT]] = None
    opset_imports = kwargs.pop("opset_imports", None)  # type: ignore
    if opset_imports is not None:
        model.opsetImport = [*opset_imports]
    else:
        # Default import
        model.opsetImport = [OperatorSetId.OperatorSetIdT()]

    functions: Optional[Sequence[Function.FunctionT]] = None
    functions = kwargs.pop("functions", None)  # type: ignore
    if functions is not None:
        model.functions = [*functions]

    for k, v in kwargs.items():
        # TODO: Does this work with repeated fields?
        setattr(model, k, v)
    return model


def make_tensor(
    name: str, data_type: int, dims: Sequence[int], vals: Any, raw: bool = False, exponents: Sequence[int] = [0]
) -> Tensor.TensorT:
    """Make a Tensor with specified arguments.  If raw is False, this
    function will choose the corresponding proto field to store the
    values based on data_type. If raw is True, use "raw_data" proto
    field to store the values, and values should be of type bytes in
    this case.

    Args:
        name (string): tensor name
        data_type (int): a value such as TensorDataType.FLOAT
        dims (List[int]): shape
        vals: values
        raw (bool): if True, vals contains the serialized content of the tensor,
            otherwise, vals should be a list of values of the type defined by *data_type*

    Returns:
        Tensor.TensorT
    """
    tensor = Tensor.TensorT()
    tensor.dataType = data_type
    tensor.name = name

    if data_type == TensorDataType.TensorDataType().STRING and raw:
        raise TypeError("Can not use raw_data to store string type.")

    np_dtype = tensor_dtype_to_np_dtype(data_type)

    # Check number of vals specified equals tensor size
    expected_size = 1
    if raw:
        expected_size = np_dtype.itemsize

    if type(vals) is np.ndarray and len(vals.shape) > 1:
        vals = vals.flatten()
    for d in dims:
        expected_size *= d

    if len(vals) != expected_size:
        raise ValueError(
            f"Number of values does not match tensor's size. Expected {expected_size}, but it is {len(vals)}. "
        )

    if raw:
        tensor.rawData = vals
    else:
        if data_type == TensorDataType.TensorDataType().FLOAT16:
            raise TypeError("Don't support FLOAT16.")
        elif data_type == TensorDataType.TensorDataType().BOOL:
            vals = np.array(vals).astype(int)
        elif data_type == TensorDataType.TensorDataType().STRING:
            vals = np.array(vals).astype(bytes)
        field = tensor_dtype_to_field(data_type)
        # getattr(tensor, field).extend(vals)
        setattr(tensor, field, vals)
    tensor.dims = [*dims]
    tensor.exponents = [*exponents]
    return tensor


def _to_bytes(value: Union[str, bytes]) -> bytes:
    """Coerce a string (or bytes) value into UTF-8 bytes."""
    return value if isinstance(value, bytes) else value.encode("utf-8")


def _to_str(value: Union[str, bytes]) -> str:
    """Coerce a string (or UTF-8 bytes) value into str."""
    return value if isinstance(value, str) else value.decode("utf-8", errors="ignore")


def make_attribute(
    key: str,
    value: Any,
    doc_string: Optional[str] = None,
    attr_type: Optional[int] = None,
) -> Attribute.AttributeT:
    """Makes an Attribute based on the value type."""
    attr = Attribute.AttributeT()
    attr.name = key
    if doc_string:
        attr.docString = doc_string

    # Singular cases
    if isinstance(value, numbers.Integral):
        attr.i = AttributeI.AttributeIT()
        attr.i.i = int(value)
        attr.attrType = AttributeType.AttributeType().INT
    elif isinstance(value, numbers.Real):
        attr.f = AttributeF.AttributeFT()
        attr.f.f = float(value)
        attr.attrType = AttributeType.AttributeType().FLOAT
    elif isinstance(value, (str, bytes)):
        # Encode strings into utf-8
        attr.s = _to_bytes(value)
        attr.attrType = AttributeType.AttributeType().STRING
    elif isinstance(value, Tensor.TensorT):
        attr.t = value
        attr.attrType = AttributeType.AttributeType().TENSOR
    elif isinstance(value, Graph.GraphT):
        attr.g = value
        attr.attrType = AttributeType.AttributeType().GRAPH
    elif isinstance(value, TypeInfo.TypeInfoT):
        attr.tp = value
        attr.attrType = AttributeType.AttributeType().TYPE_FBS
    # Iterable cases
    elif isinstance(value, collections.abc.Iterable):
        value = list(value)
        if len(value) == 0 and attr_type is None:
            raise ValueError(
                f"Could not infer attribute `{key}` type from empty iterator"
            )
        if attr_type is None:
            types = {type(v) for v in value}
            for exp_t, exp_enum in (
                (numbers.Integral, AttributeType.AttributeType().INTS),
                (numbers.Real, AttributeType.AttributeType().FLOATS),
                ((str, bytes), AttributeType.AttributeType().STRINGS),
                (Tensor.TensorT, AttributeType.AttributeType().TENSORS),
                (Graph.GraphT, AttributeType.AttributeType().GRAPHS),
                (TypeInfo.TypeInfoT, AttributeType.AttributeType().TYPE_FBSS),
            ):
                if all(issubclass(t, exp_t) for t in types):  # type: ignore[arg-type]
                    attr_type = exp_enum
                    break
            if attr_type is None:
                raise ValueError(
                    "Could not infer the attribute type from the elements of the passed Iterable value."
                )

        if attr_type == AttributeType.AttributeType().INTS:
            attr.ints = [*value]
            attr.attrType = AttributeType.AttributeType().INTS
        elif attr_type == AttributeType.AttributeType().FLOATS:
            attr.floats = [*value]
            attr.attrType = AttributeType.AttributeType().FLOATS
        elif attr_type == AttributeType.AttributeType().STRINGS:
            attr.strings = [_to_bytes(v) for v in value]
            attr.attrType = AttributeType.AttributeType().STRINGS
        elif attr_type == AttributeType.AttributeType().TENSORS:
            attr.tensors = [*value]
            attr.attrType = AttributeType.AttributeType().TENSORS
        elif attr_type == AttributeType.AttributeType().GRAPHS:
            attr.graphs = [*value]
            attr.attrType = AttributeType.AttributeType().GRAPHS
        elif attr_type == AttributeType.AttributeType().TYPE_FBSS:
            attr.typeProtos = [*value]
            attr.attrType = AttributeType.AttributeType().TYPE_FBSS
        else:
            raise AssertionError()  # Should not reach since `ValueError` must be raised in attr_type checking
    else:
        raise TypeError(f"'{value}' is not an accepted attribute value.")

    if attr_type is not None and attr.attrType != attr_type:
        raise TypeError(
            f"Inferred attribute type ({attr.attrType}) mismatched with specified type ({attr_type})"
        )
    return attr


def get_attribute_value(attr: Attribute.AttributeT) -> Any:  # noqa: PLR0911
    if attr.refAttrName:
        raise ValueError(f"Cannot get value of reference attribute: {attr}")
    if attr.attrType == AttributeType.AttributeType.FLOAT:
        return attr.f.f
    if attr.attrType == AttributeType.AttributeType.INT:
        return attr.i.i
    if attr.attrType == AttributeType.AttributeType.STRING:
        return attr.s
    if attr.attrType == AttributeType.AttributeType.TENSOR:
        return attr.t
    if attr.attrType == AttributeType.AttributeType.GRAPH:
        return attr.g
    if attr.attrType == AttributeType.AttributeType.TYPE_FBS:
        return attr.tp
    if attr.attrType == AttributeType.AttributeType.FLOATS:
        return list(attr.floats)
    if attr.attrType == AttributeType.AttributeType.INTS:
        return list(attr.ints)
    if attr.attrType == AttributeType.AttributeType.STRINGS:
        return list(attr.strings)
    if attr.attrType == AttributeType.AttributeType.TENSORS:
        return list(attr.tensors)
    if attr.attrType == AttributeType.AttributeType.GRAPHS:
        return list(attr.graphs)
    if attr.attrType == AttributeType.AttributeType.TYPE_FBSS:
        return list(attr.typeProtos)
    if attr.attrType == AttributeType.AttributeType.UNDEFINED:
        return None
    raise ValueError(f"Unsupported ONNX attribute: {attr}")


def make_tensor_type_info(
    elem_type: int,
    shape: Optional[Sequence[Union[str, int, None]]],
    shape_denotation: Optional[List[str]] = None,
) -> TypeInfo.TypeInfoT:
    tensorShape = TensorShape.TensorShapeT()
    tensorShape.dim = []

    if shape is not None:
        if shape_denotation and len(shape_denotation) != len(shape):
            raise ValueError(
                "Invalid shape_denotation. Must be of the same length as shape."
            )

        for i, d in enumerate(shape):
            dimension_value = DimensionValue.DimensionValueT()
            tensorShape_dim = Dimension.DimensionT()
            if d is None:
                pass
            elif isinstance(d, int):
                dimension_value.dimType = DimensionValueType.DimensionValueType().VALUE
                dimension_value.dimValue = d
            elif isinstance(d, str):
                dimension_value.dimType = DimensionValueType.DimensionValueType().PARAM
                dimension_value.dimParam = d
            else:
                raise ValueError(
                    f"Invalid item in shape: {d}. Needs to be of int or str."
                )

            tensorShape_dim.value = dimension_value
            if shape_denotation:
                tensorShape_dim.denotation = shape_denotation[i]

            tensorShape.dim.append(tensorShape_dim)

    value = TensorTypeAndShape.TensorTypeAndShapeT()
    value.elemType = elem_type
    value.shape = tensorShape

    """Makes a Tensor TypeInfo.TypeInfoT based on the data type and shape."""
    type_info = TypeInfo.TypeInfoT()
    type_info.valueType = TypeInfoValue.TypeInfoValue().tensor_type
    type_info.value = value

    return type_info


def make_tensor_value_info(
    name: str,
    elem_type: int,
    shape: Optional[Sequence[Union[str, int, None]]],
    doc_string: str = "",
    shape_denotation: Optional[List[str]] = None,
    exponents: Sequence[int] = [0]
) -> ValueInfo.ValueInfoT:
    """Makes a ValueInfo.ValueInfoT based on the data type and shape."""
    value_info = ValueInfo.ValueInfoT()
    value_info.name = name
    if doc_string:
        value_info.docString = doc_string

    type_info = make_tensor_type_info(elem_type, shape, shape_denotation)
    value_info.valueInfoType = type_info
    value_info.exponents = [*exponents]
    return value_info

def aes_encryptor(data: bytes):
    key = bytearray(random.getrandbits(8) for _ in range(16))
    iv = bytearray(range(16, 32, 1))
    cipher = Cipher(algorithms.AES(key), modes.CTR(iv), backend=default_backend())
    encryptor = cipher.encryptor()
    ct_data = encryptor.update(data) + encryptor.finalize()
    
    hex_key = ''
    for _, byte in enumerate(key):
        hex_key += "0x{:02x}, ".format(byte)

    return ct_data, hex_key


def aes_decryptor(data: bytes, key: bytearray):
    iv = bytearray(range(16, 32, 1))
    cipher = Cipher(algorithms.AES(key), modes.CTR(iv), backend=default_backend())
    decryptor = cipher.decryptor()
    decrypted_data = decryptor.update(data) + decryptor.finalize()
    return decrypted_data

def save(
    model: Model.ModelT, file_path: str, encrypt_data: bool = False
):
    builder = flatbuffers.Builder(1024)
    builder.Finish(model.Pack(builder))
    data = builder.Output()
    key = None
    if encrypt_data:
        data, key = aes_encryptor(data)
    
    # file header 
    header = bytearray("EDL1", 'utf-8')
    header += struct.pack("I", 1 if encrypt_data else 0)
    header += struct.pack("I", len(data))
    with open(file_path, mode="wb") as f:
        f.write(header + data)
    return key


def load(file_path: str) -> Model.ModelT:
    buf = None
    with open(file_path, mode="rb") as f:
        buf = f.read()
    return Model.ModelT.InitFromPackedBuf(buf, 0)


def _sanitize_str(s: Union[str, bytes]) -> str:
    if isinstance(s, str):
        sanitized = s
    elif isinstance(s, bytes):
        sanitized = s.decode("utf-8", errors="ignore")
    else:
        sanitized = str(s)
    if len(sanitized) < 64:  # noqa: PLR2004
        return sanitized
    return sanitized[:64] + f"...<+len={(len(sanitized) - 64)}>"


def printable_attribute(
    attr: Attribute.AttributeT, subgraphs: bool = False
) -> Union[str, Tuple[str, List[Graph.GraphT]]]:
    content = []
    content.append(_to_str(attr.name))
    content.append("=")

    def str_float(f: float) -> str:
        # NB: Different Python versions print different numbers of trailing
        # decimals, specifying this explicitly keeps it consistent for all
        # versions
        return f"{f:.15g}"

    def str_int(i: int) -> str:
        return str(i)

    _T = TypeVar("_T")

    def str_list(str_elem: Callable[[_T], str], xs: Sequence[_T]) -> str:
        return "[" + ", ".join(map(str_elem, xs)) + "]"

    # for now, this logic should continue to work as long as we are running on a flatbuffers
    # implementation. If/when we switch to flatbuffers, we will need to use attr.type

    # To support printing subgraphs, if we find a graph attribute, print out
    # its name here and pass the graph itself up to the caller for later
    # printing.
    graphs = []
    if attr.i:
        content.append(str_int(attr.i.i))
    elif attr.f:
        content.append(str_float(attr.f.f))
    elif attr.s:
        # TODO: Bit nervous about Python 2 / Python 3 determinism implications
        content.append(repr(_sanitize_str(attr.s)))
    elif attr.t:
        if len(attr.t.dims) > 0:
            content.append("<Tensor>")
        else:
            # special case to print scalars
            field = tensor_dtype_to_field(attr.t.dataType)
            content.append(f"<Scalar Tensor {getattr(attr.t, field)}>")
    elif attr.g:
        content.append(f"<graph {_to_str(attr.g.name)}>")
        graphs.append(attr.g)
    elif attr.tp:
        content.append(f"<Type Info {attr.tp}>")
    elif attr.floats:
        content.append(str_list(str_float, attr.floats))
    elif attr.ints is not None:
        content.append(str_list(str_int, attr.ints))
    elif attr.strings:
        # TODO: Bit nervous about Python 2 / Python 3 determinism implications
        content.append(str(list(map(_sanitize_str, attr.strings))))
    elif attr.tensors:
        content.append("[<Tensor>, ...]")
    elif attr.typeProtos:
        content.append("[")
        for i, tp in enumerate(attr.typeProtos):
            comma = "," if i != len(attr.typeProtos) - 1 else ""
            content.append(f"<Type Info {tp}>{comma}")
        content.append("]")
    elif attr.graphs:
        content.append("[")
        for i, g in enumerate(attr.graphs):
            comma = "," if i != len(attr.graphs) - 1 else ""
            content.append(f"<graph {_to_str(g.name)}>{comma}")
        content.append("]")
        graphs.extend(attr.graphs)
    else:
        content.append("<Unknown>")
    if subgraphs:
        return " ".join(content), graphs
    return " ".join(content)


def printable_dim(dim: Dimension.DimensionT) -> str:
    if dim.value.dimType == DimensionValueType.DimensionValueType.UNKNOWN:
        return "?"
    elif dim.value.dimType == DimensionValueType.DimensionValueType.VALUE:
        which = dim.value.dimValue
    elif dim.value.dimType == DimensionValueType.DimensionValueType.PARAM:
        which = dim.value.dimParam
    return str(which)


def get_class_attribute_name(obj: Any, value: int) -> str:
    ret: str = ""
    # 获取类和实例的所有属性名字
    all_attributes = dir(obj)
    filtered_attributes = [attr for attr in all_attributes if not attr.startswith('__') and not attr.endswith('__')]
    for attr in filtered_attributes:
        if getattr(obj, attr) == value:
            ret = attr
            break

    return ret


def printable_type(t: TypeInfo.TypeInfoT) -> str:
    if t.valueType == TypeInfoValue.TypeInfoValue.tensor_type:
        s = get_class_attribute_name(TensorDataType.TensorDataType(), t.value.elemType)
        if t.value.shape is not None:
            if len(t.value.shape.dim):
                s += str(", " + "x".join(map(printable_dim, t.value.shape.dim)))
            else:
                s += ", scalar"
        return s  # type: ignore[no-any-return]
    if t.valueType == TypeInfoValue.TypeInfoValue.NONE:
        return ""
    return f"Unknown type {t.valueType}"


def printable_value_info(v: ValueInfo.ValueInfoT) -> str:
    s = f"%{_to_str(v.name)}"
    if v.valueInfoType:
        s = f"{s}[{printable_type(v.valueInfoType)}]"
    if v.exponents:
        s += f", exponents: {repr(v.exponents)}"
    return s


def printable_tensor(t: Tensor.TensorT) -> str:
    s = f"%{_to_str(t.name)}["
    s += get_class_attribute_name(TensorDataType.TensorDataType(), t.dataType)
    if t.dims is not None:
        if len(t.dims):
            s += str(", " + "x".join(map(str, t.dims)))
        else:
            s += ", scalar"
    s += "]"
    return s


def printable_tensor_value(t: Tensor.TensorT) -> str:
    s = f"%{_to_str(t.name)}, "
    if t.dims is not None:
        s += f"shape: {repr(t.dims)}, "
    if t.exponents is not None:
        s += f"exponents: {repr(t.exponents)}, "
    if t.docString is not None:
        s += f"docString: {t.docString}, "

    if t.floatData is not None:
        s += f"value: {repr(t.floatData)}"
    elif t.int32Data is not None:
        s += f"value: {repr(t.int32Data)}"
    elif t.stringData is not None:
        s += f"value: {repr(t.stringData)}"
    elif t.int64Data is not None:
        s += f"value: {repr(t.int64Data)}"
    elif t.rawData is not None:
        np.set_printoptions(threshold=np.inf)
        s += f"\nvalue: {repr(np.frombuffer(t.rawData, dtype=tensor_dtype_to_np_dtype(t.dataType)))}"
    elif t.doubleData is not None:
        s += f"value: {repr(t.doubleData)}"
    elif t.uint64Data is not None:
        s += f"value: {repr(t.uint64Data)}"

    return s


def printable_node(
    node: Node.NodeT, prefix: str = "", subgraphs: bool = False
) -> Union[str, Tuple[str, List[Graph.GraphT]]]:
    content = []
    if len(node.output):
        content.append(", ".join([f"%{_to_str(name)}" for name in node.output]))
        content.append("=")
    # To deal with nested graphs
    graphs: List[Graph.GraphT] = []
    printed_attrs = []
    if node.attribute:
        for attr in node.attribute:
            if subgraphs:
                printed_attr_subgraphs = printable_attribute(attr, subgraphs)
                if not isinstance(printed_attr_subgraphs[1], list):
                    raise TypeError(
                        f"printed_attr_subgraphs[1] must be an instance of {list}."
                    )
                graphs.extend(printed_attr_subgraphs[1])
                printed_attrs.append(printed_attr_subgraphs[0])
            else:
                printed = printable_attribute(attr)
                if not isinstance(printed, str):
                    raise TypeError(f"printed must be an instance of {str}.")
                printed_attrs.append(printed)
    printed_attributes = ", ".join(sorted(printed_attrs))
    printed_inputs = ", ".join([f"%{_to_str(name)}" for name in node.input])
    if node.attribute:
        content.append(f"{_to_str(node.opType)}[{printed_attributes}]({printed_inputs})")
    else:
        content.append(f"{_to_str(node.opType)}({printed_inputs})")
    if subgraphs:
        return prefix + " ".join(content), graphs
    return prefix + " ".join(content)


def printable_graph(graph: Graph.GraphT, prefix: str = "", print_initializer_value: bool = False, print_value_info: bool = False, print_test_value: bool = False) -> str:
    """Display a Graph as a string.

    Args:
        graph (Graph.GraphT): the graph to display
        prefix (string): prefix of every line

    Returns:
        string
    """
    content = []
    indent = prefix + "  "
    # header
    header = ["graph", _to_str(graph.name)]
    initializers = {t.name for t in graph.initializer}
    if len(graph.input):
        header.append("(")
        in_strs = []  # required inputs
        in_with_init_strs = (
            []
        )  # optional inputs with initializer providing default value
        for inp in graph.input:
            if inp.name not in initializers:
                in_strs.append(printable_value_info(inp))
            else:
                in_with_init_strs.append(printable_value_info(inp))
        if in_strs:
            content.append(prefix + " ".join(header))
            header = []
            for line in in_strs:
                content.append(prefix + "  " + line)
        header.append(")")

        if in_with_init_strs:
            header.append("optional inputs with matching initializers (")
            content.append(prefix + " ".join(header))
            header = []
            for line in in_with_init_strs:
                content.append(prefix + "  " + line)
            header.append(")")

        # from IR 4 onwards an initializer is not required to have a matching graph input
        # so output the name, type and shape of those as well
        if len(in_with_init_strs) < len(initializers):
            graph_inputs = {i.name for i in graph.input}
            init_strs = [
                printable_tensor(i)
                for i in graph.initializer
                if i.name not in graph_inputs
            ]
            header.append("initializers (")
            content.append(prefix + " ".join(header))
            header = []
            for line in init_strs:
                content.append(prefix + "  " + line)
            header.append(")")

    header.append("{")
    content.append(prefix + " ".join(header))
    graphs: List[Graph.GraphT] = []
    # body
    for node in graph.node:
        contents_subgraphs = printable_node(node, indent, subgraphs=True)
        if not isinstance(contents_subgraphs[1], list):
            raise TypeError(f"contents_subgraphs[1] must be an instance of {list}.")
        content.append(contents_subgraphs[0])
        graphs.extend(contents_subgraphs[1])
    # tail
    tail = ["return"]
    if len(graph.output):
        tail.append(", ".join([f"%{_to_str(out.name)}" for out in graph.output]))
    content.append(indent + " ".join(tail))
    # closing bracket
    content.append(prefix + "}")
    for g in graphs:
        content.append("\n" + printable_graph(g))

    if (print_initializer_value):
        initializers_value = [
                printable_tensor_value(i)
                for i in graph.initializer
            ]
        content.append("\n\n" + "initializers value:\n" + "\n".join(initializers_value))

    if (print_value_info):
        values_info = [
                printable_value_info(i)
                for i in graph.valueInfo
            ]
        content.append("\n\n" + "values info:\n" + "\n".join(values_info))

    if (print_test_value):
        test_inputs_value = [
                printable_tensor_value(i)
                for i in graph.testInputsValue
            ]
        content.append("\n\n" + "test inputs value:\n" + "\n".join(test_inputs_value))
        test_outputs_value = [
                printable_tensor_value(i)
                for i in graph.testOutputsValue
            ]
        content.append("\n\n" + "test outputs value:\n" + "\n".join(test_outputs_value))

    return "\n".join(content)


# Following functions are used for mapping
def tensor_dtype_to_np_dtype(tensor_dtype: int) -> np.dtype:
    """Convert a TensorDataType to corresponding numpy dtype. It can be used while making tensor.

    Args:
        tensor_dtype: TensorDataType

    Returns:
        numpy's data_type
    """
    return mapping.TENSOR_TYPE_MAP[tensor_dtype].np_dtype


def tensor_dtype_to_field(tensor_dtype: int) -> str:
    """Convert a TensorDataType to corresponding field name for storage. It can be used while making tensors.

    Args:
        tensor_dtype: TensorDataType

    Returns:
        field name
    """
    return mapping._STORAGE_TENSOR_TYPE_TO_FIELD[
        mapping.TENSOR_TYPE_MAP[tensor_dtype].storage_dtype
    ]


def np_dtype_to_tensor_dtype(np_dtype: np.dtype) -> int:
    """Convert a numpy's dtype to corresponding TensorDataType. It can be used while converting numpy arrays to tensors.

    Args:
        np_dtype: numpy's data_type

    Returns:
        TensorDataType
    """
    tensor_type = mapping._NP_TYPE_TO_TENSOR_TYPE.get(np_dtype)
    if tensor_type == None:
        return cast(int, TensorDataType.TensorDataType().UNDEFINED)
    else:
        return cast(int, tensor_type)
    # return cast(
    #     int,
    #     mapping._NP_TYPE_TO_TENSOR_TYPE[np_dtype],
    # )
