# -*- coding: utf-8 -*-
# Copyright (C) 2018-2024 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

import numpy as np

"""Factory functions for all openvino ops."""
from typing import Optional, Union

from functools import partial, singledispatch

from openvino.runtime import Node, Type, PartialShape, Output, Shape
from openvino.runtime.op import assign, Constant, Parameter
from openvino.runtime.op import read_value as _read_value
from openvino.runtime.op.util import VariableInfo, Variable
from openvino.runtime.opset_utils import _get_node_factory
from openvino.runtime.utils.decorators import nameable_op, overloading
from openvino.runtime.utils.types import (
    NodeInput,
    NumericType,
    TensorShape,
    as_node,
    as_nodes,
    get_element_type,
)

_get_node_factory_opset6 = partial(_get_node_factory, "opset6")

# -------------------------------------------- ops ------------------------------------------------


@nameable_op
def ctc_greedy_decoder_seq_len(
    data: NodeInput,
    sequence_length: NodeInput,
    blank_index: Optional[NodeInput] = None,
    merge_repeated: bool = True,
    classes_index_type: str = "i32",
    sequence_length_type: str = "i32",
    name: Optional[str] = None,
) -> Node:
    """Return a node which performs CTCGreedyDecoderSeqLen.

    :param data:            The input 3D tensor. Shape: [batch_size, seq_length, num_classes]
    :param sequence_length: Input 1D tensor with sequence length. Shape: [batch_size]
    :param blank_index:     Scalar or 1D tensor with specifies the class index to use for the blank class.
                            Optional parameter. Default value is num_classes-1.
    :return:                The new node which performs CTCGreedyDecoderSeqLen.
    """
    if blank_index is not None:
        inputs = as_nodes(data, sequence_length, blank_index, name=name)
    else:
        inputs = as_nodes(data, sequence_length, name=name)

    attributes = {
        "merge_repeated": merge_repeated,
        "classes_index_type": classes_index_type,
        "sequence_length_type": sequence_length_type,
    }

    return _get_node_factory_opset6().create("CTCGreedyDecoderSeqLen", inputs, attributes)


@nameable_op
def gather_elements(
    data: NodeInput,
    indices: NodeInput,
    axis: Optional[int] = 0,
    name: Optional[str] = None,
) -> Node:
    """Return a node which performs GatherElements.

    :param data:       N-D tensor with data for gathering
    :param indices:    N-D tensor with indices by which data is gathered
    :param axis:       axis along which elements are gathered
    :return:           The new node which performs GatherElements
    """
    inputs = as_nodes(data, indices, name=name)

    attributes = {
        "axis": axis,
    }

    return _get_node_factory_opset6().create("GatherElements", inputs, attributes)


@nameable_op
def mvn(
    data: Node,
    axes: Node,
    normalize_variance: bool,
    eps: float,
    eps_mode: str,
    name: Optional[str] = None,
) -> Node:
    """Return a node which performs MeanVarianceNormalization (MVN).

    :param data: The node with data tensor.
    :param axes: The node with axes to reduce on.
    :param normalize_variance: Denotes whether to perform variance normalization.
    :param eps: The number added to the variance to avoid division by zero
               when normalizing the value. Scalar value.
    :param eps_mode: how eps is applied (`inside_sqrt` or `outside_sqrt`)
    :param name: Optional output node name.
    :return: The new node performing a MVN operation on input tensor.
    """
    inputs = as_nodes(data, axes, name=name)

    attributes = {
        "normalize_variance": normalize_variance,
        "eps": eps,
        "eps_mode": eps_mode,
    }

    return _get_node_factory_opset6().create("MVN", inputs, attributes)


@overloading(Union[Node, Output, int, float, np.ndarray], str, Optional[Union[type, np.dtype, Type, str]],
             Optional[Union[TensorShape, Shape, PartialShape]], Optional[str])
@nameable_op
def read_value(init_value: Union[Node, Output, int, float, np.ndarray],
               variable_id: str,
               variable_type: Optional[Union[type, np.dtype, Type, str]] = None,
               variable_shape: Optional[Union[TensorShape, Shape, PartialShape]] = None,
               name: Optional[str] = None) -> Node:
    """Return a node which produces the Assign operation.

    :param init_value:   Node producing a value to be returned instead of an unassigned variable.
    :param variable_id:  Id of a variable to be read.
    :param variable_type:   Optional type to be set into Variable.
    :param variable_shape:  Optional shape to be set into Variable.
    :param name:         Optional name for output node.
    :return: ReadValue node
    """
    info = VariableInfo()
    info.variable_id = variable_id

    if variable_type is not None:
        if not isinstance(variable_type, Type) and not isinstance(variable_type, str):
            info.data_type = get_element_type(variable_type)
        else:
            info.data_type = variable_type
    else:
        info.data_type = Type.dynamic

    if variable_shape is not None:
        info.data_shape = PartialShape(variable_shape)
    else:
        info.data_shape = PartialShape.dynamic()

    var_from_info = Variable(info)
    return _read_value(new_value=as_node(init_value, name=name), variable=var_from_info)


@overloading(str, Optional[Union[type, np.dtype, Type, str]], Optional[Union[TensorShape, Shape, PartialShape]], Optional[str])  # type: ignore
@nameable_op
def read_value(variable_id: str,  # noqa: F811
               variable_type: Optional[Union[type, np.dtype, Type, str]] = None,
               variable_shape: Optional[Union[TensorShape, Shape, PartialShape]] = None,
               name: Optional[str] = None) -> Node:
    """Return a node which produces the Assign operation.

    :param variable_id:  Id of a variable to be read.
    :param variable_type:   Optional type to be set into Variable.
    :param variable_shape:  Optional shape to be set into Variable.
    :param name:         Optional name for output node.
    :return: ReadValue node
    """
    info = VariableInfo()
    info.variable_id = variable_id

    if variable_type is not None:
        if not isinstance(variable_type, Type) and not isinstance(variable_type, str):
            info.data_type = get_element_type(variable_type)
        else:
            info.data_type = variable_type
    else:
        info.data_type = Type.dynamic

    if variable_shape is not None:
        info.data_shape = PartialShape(variable_shape)
    else:
        info.data_shape = PartialShape.dynamic()

    var_from_info = Variable(info)

    return _read_value(var_from_info)


@overloading(Variable, Optional[str])    # type: ignore
@nameable_op
def read_value(ov_variable: Variable,  # noqa: F811
               name: Optional[str] = None) -> Node:
    """Return a node which produces the Assign operation.

    :param ov_variable:  Variable to be read.
    :param name:         Optional name for output node.
    :return: ReadValue node
    """
    return _read_value(ov_variable)


@overloading(Union[Node, Output, int, float, np.ndarray], Variable, Optional[str])  # type: ignore
@nameable_op
def read_value(init_value: Union[Node, Output, int, float, np.ndarray],  # noqa: F811
               ov_variable: Variable,
               name: Optional[str] = None) -> Node:
    """Return a node which produces the Assign operation.

    :param init_value:   Optional node producing a value to be returned instead of an unassigned variable.
    :param ov_variable:  Variable to be read.
    :param name:         Optional name for output node.
    :return: ReadValue node
    """
    return _read_value(as_node(init_value, name=name), ov_variable)
