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

import numpy as np

import onnx
from onnx.backend.test.case.base import Base
from onnx.backend.test.case.node import expect


def specify_int64(indices, inverse_indices, counts):
    return (
        np.array(indices, dtype=np.int64),
        np.array(inverse_indices, dtype=np.int64),
        np.array(counts, dtype=np.int64),
    )


class Unique(Base):
    @staticmethod
    def export_sorted_without_axis() -> None:
        node_sorted = onnx.helper.make_node(
            "Unique",
            inputs=["X"],
            outputs=["Y", "indices", "inverse_indices", "counts"],
        )

        x = np.array([2.0, 1.0, 1.0, 3.0, 4.0, 3.0], dtype=np.float32)
        y, indices, inverse_indices, counts = np.unique(x, True, True, True)
        indices, inverse_indices, counts = specify_int64(
            indices, inverse_indices, counts
        )
        expect(
            node_sorted,
            inputs=[x],
            outputs=[y, indices, inverse_indices, counts],
            name="test_unique_sorted_without_axis",
        )

    @staticmethod
    def export_not_sorted_without_axis() -> None:
        node_not_sorted = onnx.helper.make_node(
            "Unique",
            inputs=["X"],
            outputs=["Y", "indices", "inverse_indices", "counts"],
            sorted=0,
        )
        # numpy unique does not retain original order (it sorts the output unique values)
        # https://github.com/numpy/numpy/issues/8621
        # we need to recover unsorted output and indices
        x = np.array([2.0, 1.0, 1.0, 3.0, 4.0, 3.0], dtype=np.float32)
        y, indices, inverse_indices, counts = np.unique(x, True, True, True)

        # prepare index mapping from sorted to unsorted
        argsorted_indices = np.argsort(indices)
        inverse_indices_map = dict(
            zip(argsorted_indices, np.arange(len(argsorted_indices)), strict=True)
        )

        indices = indices[argsorted_indices]
        y = np.take(x, indices, axis=0)
        inverse_indices = np.asarray(
            [inverse_indices_map[i] for i in inverse_indices], dtype=np.int64
        )
        counts = counts[argsorted_indices]
        indices, inverse_indices, counts = specify_int64(
            indices, inverse_indices, counts
        )
        # print(y)
        # [2.0, 1.0, 3.0, 4.0]
        # print(indices)
        # [0 1 3 4]
        # print(inverse_indices)
        # [0, 1, 1, 2, 3, 2]
        # print(counts)
        # [1, 2, 2, 1]

        expect(
            node_not_sorted,
            inputs=[x],
            outputs=[y, indices, inverse_indices, counts],
            name="test_unique_not_sorted_without_axis",
        )

    @staticmethod
    def export_sorted_with_axis() -> None:
        node_sorted = onnx.helper.make_node(
            "Unique",
            inputs=["X"],
            outputs=["Y", "indices", "inverse_indices", "counts"],
            sorted=1,
            axis=0,
        )

        x = np.array([[1, 0, 0], [1, 0, 0], [2, 3, 4]], dtype=np.float32)
        y, indices, inverse_indices, counts = np.unique(x, True, True, True, axis=0)
        indices, inverse_indices, counts = specify_int64(
            indices, inverse_indices, counts
        )
        # behavior changed with numpy >= 2.0
        inverse_indices = inverse_indices.reshape(-1)
        # print(y)
        # [[1. 0. 0.]
        #  [2. 3. 4.]]
        # print(indices)
        # [0 2]
        # print(inverse_indices)
        # [0 0 1]
        # print(counts)
        # [2 1]

        expect(
            node_sorted,
            inputs=[x],
            outputs=[y, indices, inverse_indices, counts],
            name="test_unique_sorted_with_axis",
        )

    @staticmethod
    def export_sorted_with_axis_3d() -> None:
        node_sorted = onnx.helper.make_node(
            "Unique",
            inputs=["X"],
            outputs=["Y", "indices", "inverse_indices", "counts"],
            sorted=1,
            axis=1,
        )

        x = np.array(
            [
                [[1.0, 1.0], [0.0, 1.0], [2.0, 1.0], [0.0, 1.0]],
                [[1.0, 1.0], [0.0, 1.0], [2.0, 1.0], [0.0, 1.0]],
            ],
            dtype=np.float32,
        )
        y, indices, inverse_indices, counts = np.unique(x, True, True, True, axis=1)
        indices, inverse_indices, counts = specify_int64(
            indices, inverse_indices, counts
        )
        # behavior changed with numpy >= 2.0
        inverse_indices = inverse_indices.reshape(-1)
        # print(y)
        # [[[0. 1.]
        #  [1. 1.]
        #  [2. 1.]]
        # [[0. 1.]
        #  [1. 1.]
        #  [2. 1.]]]
        # print(indices)
        # [1 0 2]
        # print(inverse_indices)
        # [1 0 2 0]
        # print(counts)
        # [2 1 1]
        expect(
            node_sorted,
            inputs=[x],
            outputs=[y, indices, inverse_indices, counts],
            name="test_unique_sorted_with_axis_3d",
        )

    @staticmethod
    def export_sorted_with_negative_axis() -> None:
        node_sorted = onnx.helper.make_node(
            "Unique",
            inputs=["X"],
            outputs=["Y", "indices", "inverse_indices", "counts"],
            sorted=1,
            axis=-1,
        )

        x = np.array([[1, 0, 0], [1, 0, 0], [2, 3, 3]], dtype=np.float32)
        y, indices, inverse_indices, counts = np.unique(x, True, True, True, axis=-1)
        indices, inverse_indices, counts = specify_int64(
            indices, inverse_indices, counts
        )
        # behavior changed with numpy >= 2.0
        inverse_indices = inverse_indices.reshape(-1)
        # print(y)
        # [[0. 1.]
        #  [0. 1.]
        #  [3. 2.]]
        # print(indices)
        # [1 0]
        # print(inverse_indices)
        # [1 0 0]
        # print(counts)
        # [2 1]

        expect(
            node_sorted,
            inputs=[x],
            outputs=[y, indices, inverse_indices, counts],
            name="test_unique_sorted_with_negative_axis",
        )

    @staticmethod
    def export_length_1() -> None:
        node_sorted = onnx.helper.make_node(
            "Unique",
            inputs=["X"],
            outputs=["Y", "indices", "inverse_indices", "counts"],
            sorted=1,
        )

        x = np.array([0], dtype=np.int64)
        y, indices, inverse_indices, counts = np.unique(x, True, True, True)
        indices, inverse_indices, counts = specify_int64(
            indices, inverse_indices, counts
        )
        # behavior changed with numpy >= 2.0
        inverse_indices = inverse_indices.reshape(-1)
        # print(y)
        # [0]
        # print(indices)
        # [0]
        # print(inverse_indices)
        # [0]
        # print(counts)
        # [1]

        expect(
            node_sorted,
            inputs=[x],
            outputs=[y, indices, inverse_indices, counts],
            name="test_unique_length_1",
        )
