# Copyright (c) ONNX Project Contributors
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations

import sys

import numpy as np

import onnx
import onnx._custom_element_types as custom
from onnx import TensorProto, helper, subbyte
from onnx.backend.test.case.base import Base
from onnx.backend.test.case.node import expect
from onnx.helper import (
    float32_to_float8e4m3,
    float32_to_float8e5m2,
    make_tensor,
    tensor_dtype_to_field,
)
from onnx.numpy_helper import (
    float8e4m3_to_float32,
    float8e5m2_to_float32,
    unpacked_float4e2m1_to_float32,
)


class Cast(Base):
    @staticmethod
    def export() -> None:
        shape = (3, 4)
        test_cases = [
            ("FLOAT", "FLOAT16"),
            ("FLOAT", "DOUBLE"),
            ("FLOAT16", "FLOAT"),
            ("FLOAT16", "DOUBLE"),
            ("DOUBLE", "FLOAT"),
            ("DOUBLE", "FLOAT16"),
            ("FLOAT", "STRING"),
            ("STRING", "FLOAT"),
            ("FLOAT", "BFLOAT16"),
            ("BFLOAT16", "FLOAT"),
            ("FLOAT", "FLOAT8E4M3FN"),
            ("FLOAT16", "FLOAT8E4M3FN"),
            ("FLOAT", "FLOAT8E4M3FNUZ"),
            ("FLOAT16", "FLOAT8E4M3FNUZ"),
            ("FLOAT8E4M3FN", "FLOAT"),
            ("FLOAT8E4M3FN", "FLOAT16"),
            ("FLOAT8E4M3FNUZ", "FLOAT"),
            ("FLOAT8E4M3FNUZ", "FLOAT16"),
            ("FLOAT", "FLOAT8E5M2"),
            ("FLOAT16", "FLOAT8E5M2"),
            ("FLOAT", "FLOAT8E5M2FNUZ"),
            ("FLOAT16", "FLOAT8E5M2FNUZ"),
            ("FLOAT8E5M2", "FLOAT"),
            ("FLOAT8E5M2", "FLOAT16"),
            ("FLOAT8E5M2FNUZ", "FLOAT"),
            ("FLOAT8E5M2FNUZ", "FLOAT16"),
            ("FLOAT", "UINT4"),
            ("FLOAT16", "UINT4"),
            ("FLOAT", "INT4"),
            ("FLOAT16", "INT4"),
            ("UINT4", "FLOAT"),
            ("UINT4", "FLOAT16"),
            ("UINT4", "UINT8"),
            ("INT4", "FLOAT"),
            ("INT4", "FLOAT16"),
            ("INT4", "INT8"),
            ("FLOAT4E2M1", "FLOAT"),
            ("FLOAT4E2M1", "FLOAT16"),
            ("FLOAT", "FLOAT4E2M1"),
            ("FLOAT16", "FLOAT4E2M1"),
        ]

        vect_float32_to_float8e4m3 = np.vectorize(float32_to_float8e4m3)
        vect_float32_to_float8e5m2 = np.vectorize(float32_to_float8e5m2)
        vect_float32_to_uint4 = np.vectorize(
            lambda x: subbyte.float32_to_4bit_unpacked(x, signed=False)
        )
        vect_float32_to_int4 = np.vectorize(
            lambda x: subbyte.float32_to_4bit_unpacked(x, signed=True)
        )

        f8_types = ("FLOAT8E4M3FN", "FLOAT8E4M3FNUZ", "FLOAT8E5M2", "FLOAT8E5M2FNUZ")

        for from_type, to_type in test_cases:
            input_type_proto = None
            output_type_proto = None
            if from_type == "BFLOAT16" or to_type == "BFLOAT16":
                np_fp32 = np.array(
                    [
                        "0.47892547",
                        "0.48033667",
                        "0.49968487",
                        "0.81910545",
                        "0.47031248",
                        "0.816468",
                        "0.21087195",
                        "0.7229038",
                        "NaN",
                        "INF",
                        "+INF",
                        "-INF",
                    ],
                    dtype=np.float32,
                )
                little_endisan = sys.byteorder == "little"
                np_uint16_view = np_fp32.view(dtype=np.uint16)
                np_bfp16 = (
                    np_uint16_view[1::2] if little_endisan else np_uint16_view[0::2]
                )
                if to_type == "BFLOAT16":
                    assert from_type == "FLOAT"
                    input = np_fp32.reshape([3, 4])
                    output = np_bfp16.reshape([3, 4])
                    input_type_proto = onnx.helper.make_tensor_type_proto(
                        int(TensorProto.FLOAT), input.shape
                    )
                    output_type_proto = onnx.helper.make_tensor_type_proto(
                        int(TensorProto.BFLOAT16), output.shape
                    )
                else:
                    assert to_type == "FLOAT"
                    input = np_bfp16.reshape([3, 4])
                    # convert bfloat to FLOAT
                    np_fp32_zeros = np.zeros((len(np_bfp16) * 2,), dtype=np.uint16)
                    if little_endisan:
                        np_fp32_zeros[1::2] = np_bfp16
                    else:
                        np_fp32_zeros[0::2] = np_bfp16
                    np_fp32_from_bfloat = np_fp32_zeros.view(dtype=np.float32)
                    output = np_fp32_from_bfloat.reshape([3, 4])
                    input_type_proto = onnx.helper.make_tensor_type_proto(
                        int(TensorProto.BFLOAT16), input.shape
                    )
                    output_type_proto = onnx.helper.make_tensor_type_proto(
                        int(TensorProto.FLOAT), output.shape
                    )
            elif from_type in f8_types or to_type in f8_types:
                np_fp32 = np.array(
                    [
                        "0.47892547",
                        "0.48033667",
                        "0.49968487",
                        "0.81910545",
                        "0.47031248",
                        "0.7229038",
                        "1000000",
                        "1e-7",
                        "NaN",
                        "INF",
                        "+INF",
                        "-INF",
                        "-0.0000001",
                        "0.0000001",
                        "-1000000",
                    ],
                    dtype=np.float32,
                )

                if from_type == "FLOAT":
                    input_values = np_fp32
                    input = make_tensor(
                        "x", TensorProto.FLOAT, [3, 5], np_fp32.tolist()
                    )
                elif from_type == "FLOAT16":
                    input_values = np_fp32.astype(np.float16).astype(np.float32)
                    input = make_tensor(
                        "x", TensorProto.FLOAT16, [3, 5], input_values.tolist()
                    )
                elif from_type == "FLOAT8E4M3FN":
                    input_values = float8e4m3_to_float32(
                        vect_float32_to_float8e4m3(np_fp32)
                    )
                    input = make_tensor(
                        "x", TensorProto.FLOAT8E4M3FN, [3, 5], input_values.tolist()
                    )
                elif from_type == "FLOAT8E4M3FNUZ":
                    input_values = float8e4m3_to_float32(
                        vect_float32_to_float8e4m3(np_fp32, uz=True), uz=True
                    )
                    input = make_tensor(
                        "x", TensorProto.FLOAT8E4M3FNUZ, [3, 5], input_values.tolist()
                    )
                elif from_type == "FLOAT8E5M2":
                    input_values = float8e5m2_to_float32(
                        vect_float32_to_float8e5m2(np_fp32)
                    )
                    input = make_tensor(
                        "x", TensorProto.FLOAT8E5M2, [3, 5], input_values.tolist()
                    )
                elif from_type == "FLOAT8E5M2FNUZ":
                    input_values = float8e5m2_to_float32(
                        vect_float32_to_float8e5m2(np_fp32, fn=True, uz=True),
                        fn=True,
                        uz=True,
                    )
                    input = make_tensor(
                        "x", TensorProto.FLOAT8E5M2FNUZ, [3, 5], input_values.tolist()
                    )
                else:
                    raise ValueError(
                        "Conversion from {from_type} to {to_type} is not tested."
                    )

                if to_type == "FLOAT8E4M3FN":
                    expected = float8e4m3_to_float32(
                        vect_float32_to_float8e4m3(input_values)
                    )
                elif to_type == "FLOAT8E4M3FNUZ":
                    expected = float8e4m3_to_float32(
                        vect_float32_to_float8e4m3(input_values, uz=True), uz=True
                    )
                elif to_type == "FLOAT8E5M2":
                    expected = float8e5m2_to_float32(
                        vect_float32_to_float8e5m2(input_values)
                    )
                elif to_type == "FLOAT8E5M2FNUZ":
                    expected = float8e5m2_to_float32(
                        vect_float32_to_float8e5m2(input_values, fn=True, uz=True),
                        fn=True,
                        uz=True,
                    )
                elif to_type == "FLOAT16":
                    expected = input_values.astype(np.float16).astype(np.float32)
                elif to_type == "FLOAT":
                    expected = input_values
                else:
                    raise ValueError(
                        "Conversion from {from_type} to {to_type} is not tested."
                    )
                expected_tensor = make_tensor(
                    "x", getattr(TensorProto, to_type), [3, 5], expected.tolist()
                )
                output = expected_tensor
            elif from_type in ("UINT4", "INT4") or to_type in ("UINT4", "INT4"):
                np_fp32 = np.arange(-9, 16).astype(np.float32)
                input_shape = (5, 5)
                if from_type == "FLOAT":
                    input_values = np_fp32
                    input = make_tensor(
                        "x", TensorProto.FLOAT, input_shape, input_values.tolist()
                    )
                elif from_type == "FLOAT16":
                    input_values = np_fp32.astype(np.float16)
                    input = make_tensor(
                        "x", TensorProto.FLOAT16, input_shape, input_values.tolist()
                    )
                elif from_type == "UINT4":
                    input_values = vect_float32_to_uint4(np_fp32)
                    input = make_tensor(
                        "x", TensorProto.UINT4, input_shape, input_values.tolist()
                    )
                elif from_type == "INT4":
                    input_values = vect_float32_to_int4(np_fp32)
                    input = make_tensor(
                        "x", TensorProto.INT4, input_shape, input_values.tolist()
                    )
                else:
                    raise ValueError(
                        "Conversion from {from_type} to {to_type} is not tested."
                    )
                if to_type == "UINT4":
                    expected = vect_float32_to_uint4(input_values).astype(custom.uint4)
                elif to_type == "INT4":
                    expected = vect_float32_to_int4(input_values).astype(custom.int4)
                elif to_type == "FLOAT16":
                    expected = input_values.astype(np.float16)
                elif to_type == "FLOAT":
                    expected = input_values
                elif to_type == "UINT8":
                    expected = input_values.astype(np.uint8)
                elif to_type == "INT8":
                    expected = input_values.astype(np.int8)
                else:
                    raise ValueError(
                        "Conversion from {from_type} to {to_type} is not tested."
                    )
                expected_tensor = make_tensor(
                    "y", getattr(TensorProto, to_type), input_shape, expected.tolist()
                )
                output = expected_tensor
                input_type_proto = onnx.helper.make_tensor_type_proto(
                    getattr(TensorProto, from_type), input_shape
                )
                output_type_proto = onnx.helper.make_tensor_type_proto(
                    getattr(TensorProto, to_type), input_shape
                )
            elif from_type == "FLOAT4E2M1" or to_type == "FLOAT4E2M1":
                np_fp32 = np.array(
                    [
                        "0.48",
                        "0.25",
                        "1.05",
                        "-3.5",
                        "-8",
                        "9",
                        "1000000",
                        "1e-7",
                        "NaN",
                        "INF",
                        "+INF",
                        "-INF",
                        "-4",
                        "0.01",
                        "-0.0",
                    ],
                    dtype=np.float32,
                )
                input_shape = (3, 5)
                if from_type == "FLOAT":
                    input_values = np_fp32
                    input = make_tensor(
                        "x", TensorProto.FLOAT, input_shape, input_values.tolist()
                    )
                elif from_type == "FLOAT16":
                    input_values = np_fp32.astype(np.float16).astype(np.float32)
                    input = make_tensor(
                        "x", TensorProto.FLOAT16, input_shape, input_values.tolist()
                    )
                elif from_type == "FLOAT4E2M1":
                    input = make_tensor(
                        "x", TensorProto.FLOAT4E2M1, input_shape, np_fp32.tolist()
                    )
                else:
                    raise ValueError(
                        f"Conversion from {from_type} to {to_type} is not tested."
                    )

                if to_type not in ("FLOAT", "FLOAT16", "FLOAT4E2M1"):
                    raise ValueError(
                        f"Conversion from {from_type} to {to_type} is not tested."
                    )
                expected = unpacked_float4e2m1_to_float32(
                    subbyte.float32_to_float4e2m1_unpacked(np_fp32)
                )
                output = make_tensor(
                    "y", getattr(TensorProto, to_type), input_shape, expected.tolist()
                )
            elif from_type != "STRING":
                input = np.random.random_sample(shape).astype(
                    helper.tensor_dtype_to_np_dtype(getattr(TensorProto, from_type))
                )
                if to_type == "STRING":
                    # Converting input to str, then give it object dtype for generating script
                    ss = []
                    for i in input.flatten():
                        s = str(i).encode("utf-8")
                        su = s.decode("utf-8")
                        ss.append(su)

                    output = np.array(ss).astype(object).reshape([3, 4])
                else:
                    output = input.astype(
                        helper.tensor_dtype_to_np_dtype(getattr(TensorProto, to_type))
                    )
            else:
                input = np.array(
                    [
                        "0.47892547",
                        "0.48033667",
                        "0.49968487",
                        "0.81910545",
                        "0.47031248",
                        "0.816468",
                        "0.21087195",
                        "0.7229038",
                        "NaN",
                        "INF",
                        "+INF",
                        "-INF",
                    ],
                    dtype=np.dtype(object),
                ).reshape([3, 4])
                output = input.astype(
                    helper.tensor_dtype_to_np_dtype(getattr(TensorProto, to_type))
                )
            node = onnx.helper.make_node(
                "Cast",
                inputs=["input"],
                outputs=["output"],
                to=getattr(TensorProto, to_type),
            )
            if input_type_proto and output_type_proto:
                expect(
                    node,
                    inputs=[input],
                    outputs=[output],
                    name="test_cast_" + from_type + "_to_" + to_type,
                    input_type_protos=[input_type_proto],
                    output_type_protos=[output_type_proto],
                )
            else:
                expect(
                    node,
                    inputs=[input],
                    outputs=[output],
                    name="test_cast_" + from_type + "_to_" + to_type,
                )

    @staticmethod
    def export_saturate_false() -> None:
        test_cases = [
            ("FLOAT", "FLOAT8E4M3FN"),
            ("FLOAT16", "FLOAT8E4M3FN"),
            ("FLOAT", "FLOAT8E4M3FNUZ"),
            ("FLOAT16", "FLOAT8E4M3FNUZ"),
            ("FLOAT", "FLOAT8E5M2"),
            ("FLOAT16", "FLOAT8E5M2"),
            ("FLOAT", "FLOAT8E5M2FNUZ"),
            ("FLOAT16", "FLOAT8E5M2FNUZ"),
        ]
        vect_float32_to_float8e4m3 = np.vectorize(float32_to_float8e4m3)
        vect_float32_to_float8e5m2 = np.vectorize(float32_to_float8e5m2)

        for from_type, to_type in test_cases:
            np_fp32 = np.array(
                [
                    "0.47892547",
                    "0.48033667",
                    "0.49968487",
                    "0.81910545",
                    "0.47031248",
                    "0.7229038",
                    "1000000",
                    "1e-7",
                    "NaN",
                    "INF",
                    "+INF",
                    "-INF",
                    "-0.0000001",
                    "0.0000001",
                    "-1000000",
                ],
                dtype=np.float32,
            )

            if from_type == "FLOAT":
                input_values = np_fp32
                input = make_tensor("x", TensorProto.FLOAT, [3, 5], np_fp32.tolist())
            elif from_type == "FLOAT16":
                input_values = np_fp32.astype(np.float16).astype(np.float32)
                input = make_tensor(
                    "x", TensorProto.FLOAT16, [3, 5], input_values.tolist()
                )
            else:
                raise ValueError(
                    "Conversion from {from_type} to {to_type} is not tested."
                )

            if to_type == "FLOAT8E4M3FN":
                expected = vect_float32_to_float8e4m3(input_values, saturate=False)
            elif to_type == "FLOAT8E4M3FNUZ":
                expected = vect_float32_to_float8e4m3(
                    input_values, uz=True, saturate=False
                )
            elif to_type == "FLOAT8E5M2":
                expected = vect_float32_to_float8e5m2(input_values, saturate=False)
            elif to_type == "FLOAT8E5M2FNUZ":
                expected = vect_float32_to_float8e5m2(
                    input_values, fn=True, uz=True, saturate=False
                )
            else:
                raise ValueError(
                    "Conversion from {from_type} to {to_type} is not tested."
                )

            ivals = bytes([int(i) for i in expected])
            tensor = TensorProto()
            tensor.data_type = getattr(TensorProto, to_type)
            tensor.name = "x"
            tensor.dims.extend([3, 5])
            field = tensor_dtype_to_field(tensor.data_type)
            getattr(tensor, field).extend(ivals)

            output = tensor

            node = onnx.helper.make_node(
                "Cast",
                inputs=["input"],
                outputs=["output"],
                to=getattr(TensorProto, to_type),
                saturate=0,
            )
            expect(
                node,
                inputs=[input],
                outputs=[output],
                name="test_cast_no_saturate_" + from_type + "_to_" + to_type,
            )
