# SPDX-FileCopyrightText: Copyright 2020-2023 Arm Limited and/or its affiliates <open-source-office@arm.com>
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the License); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an AS IS BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Description:
# Utility functions for creating Network Operations.
from typing import Optional
from typing import Tuple

import numpy as np

from .data_type import DataType
from .high_level_command_to_npu_op import ifm_ifm2_correct_order
from .operation import ActivationFunction
from .operation import Op
from .operation import Operation
from .operation import Padding
from .reader_util import clone_and_reshape_tensor
from .shape4d import Shape4D
from .tensor import create_const_tensor
from .tensor import create_equivalence_id
from .tensor import QuantizationParameters
from .tensor import Tensor


def create_avgpool_nop(name: str) -> Operation:
    op = Operation(Op.AvgPool, name)
    op.attrs["padding"] = Padding.VALID
    op.attrs["stride_w"] = 1
    op.attrs["stride_h"] = 1
    op.attrs["filter_width"] = 1
    op.attrs["filter_height"] = 1
    op.attrs["strides"] = [1, 1, 1, 1]
    op.attrs["ksize"] = [1, 1, 1, 1]
    op.attrs["skirt"] = [0, 0, 0, 0]
    op.attrs["explicit_padding"] = [0, 0, 0, 0]  # [top, left, bottom, right]
    op.run_on_npu = True
    return op


def create_add_nop(name: str) -> Operation:
    op = Operation(Op.Add, name)
    op.run_on_npu = True
    return op


def create_memcpy(
    name: str,
    ifm: Tensor,
    ofm: Tensor,
) -> Operation:
    op = Operation(Op.Memcpy, name)
    op.run_on_npu = True
    op.add_input_tensor(ifm)
    op.set_output_tensor(ofm)
    op.set_ifm_ofm_shapes()
    return op


def create_pad_nop(name: str) -> Operation:
    op = Operation(Op.Pad, name)
    op.run_on_npu = True
    return op


def create_cast_op(
    name: str,
    ifm: Tensor,
    ofm: Tensor,
) -> Operation:
    op = Operation(Op.DepthwiseConv2DBias, name)
    op_attrs = {
        "padding": Padding.VALID,
        "stride_h": 1,
        "stride_w": 1,
        "strides": (1, 1, 1, 1),
        "depth_multiplier": 1,
        "channel_multiplier": 1,
        "dilation_h_factor": 1,
        "dilation_w_factor": 1,
        "dilation": (1, 1, 1, 1),
        "explicit_padding": None,
    }
    op.attrs.update(op_attrs)
    op.add_input_tensor(ifm)

    c = ifm.shape[-1]

    shape = [1, 1, 1, c]
    kernel = np.dstack([1] * c)
    identity_quant = QuantizationParameters(scale_f32=1.0, zero_point=0)
    op.add_input_tensor(
        create_const_tensor(
            op.name + "_weights",
            shape,
            DataType.uint8,
            np.array(kernel).reshape(shape),
            quantization=identity_quant,
        ),
    )
    bias_values = [0] * c
    dtype = DataType.int64 if op.ifm.dtype == DataType.int16 else DataType.int32
    op.add_input_tensor(create_const_tensor(op.name + "_bias", [c], dtype, bias_values))
    op.set_output_tensor(ofm)
    op.set_ifm_ofm_shapes()

    return op


def create_fused_activation(op_type: Op, name: str, ifm: Tensor, quantization: QuantizationParameters) -> Operation:
    assert op_type.is_activation_op()
    op = create_avgpool_nop(name)
    op.activation = ActivationFunction(op_type)
    ofm = Tensor(ifm.shape, ifm.dtype, f"{op.name}_tens0")
    ofm.quantization = quantization
    op.add_input_tensor(ifm)
    op.set_output_tensor(ofm)
    op.set_ifm_ofm_shapes()
    return op


def create_fullyconnected(
    name: str,
    ifm: Tensor,
    weights: Tensor,
    bias: Optional[Tensor],
    quantization: QuantizationParameters,
    vela_weight_order: bool = True,
) -> Operation:
    # Reshape weights if needed
    if not vela_weight_order:
        weights = clone_and_reshape_tensor(weights, (1, 0), False)

    n_ofm = weights.shape[-1]

    # Setup bias if needed
    if not bias:
        bias_values = [0] * n_ofm
        dtype = DataType.int64 if ifm.dtype == DataType.int16 else DataType.int32
        bias = create_const_tensor(f"{name}_bias", [n_ofm], dtype, bias_values)
        # Set equivalence_id based on values to avoid placing duplicate data in flash
        bias.equivalence_id = create_equivalence_id(tuple(bias_values))
        bias.value_id = bias.equivalence_id

    # Setup ofm
    ofm = Tensor([ifm.shape[0], n_ofm], ifm.dtype, f"{name}_tens0")
    ofm.quantization = quantization

    # Create op and add tensors
    op = Operation(Op.FullyConnected, name)
    op.add_input_tensor(ifm)
    op.add_input_tensor(weights)
    op.add_input_tensor(bias)
    op.set_output_tensor(ofm)
    op.set_ifm_ofm_shapes()
    return op


def create_depthwise_maxpool(
    name: str,
    ifm: Tensor,
    inp_shape: Shape4D,
    quantization: QuantizationParameters,
    activation: Optional[ActivationFunction] = None,
) -> Operation:
    op = Operation(Op.MaxPool, name)
    height = inp_shape.height * inp_shape.width
    width = inp_shape.depth
    ifm_shape = Shape4D([1, height, width, 1])

    op.attrs["padding"] = Padding.VALID
    op.attrs["stride_w"] = 1
    op.attrs["stride_h"] = 1
    op.attrs["filter_width"] = width
    op.attrs["filter_height"] = 1
    op.attrs["strides"] = [1, op.attrs["stride_h"], op.attrs["stride_w"], 1]
    op.attrs["ksize"] = [1, op.attrs["filter_height"], op.attrs["filter_width"], 1]
    op.activation = activation
    op.inputs = [ifm]
    ofm = Tensor([1, height, 1, 1], ifm.dtype, op.name + "_tens0")
    ofm.quantization = quantization
    op.set_output_tensor(ofm)
    op.ifm_shapes.append(ifm_shape)
    op.ofm_shapes.append(Shape4D(ofm.shape))
    return op


def create_reduce_sum(
    name: str, ifm: Tensor, quantization: QuantizationParameters, activation: Optional[ActivationFunction] = None
) -> Operation:
    op = Operation(Op.ReduceSum, name)
    op.attrs["padding"] = Padding.VALID
    op.attrs["stride_w"] = 1
    op.attrs["stride_h"] = 1
    op.attrs["filter_width"] = 1
    op.attrs["filter_height"] = 1
    op.attrs["strides"] = [1, op.attrs["stride_h"], op.attrs["stride_w"], 1]
    op.attrs["ksize"] = [1, op.attrs["filter_height"], op.attrs["filter_width"], 1]
    op.add_input_tensor(ifm)
    op.activation = activation
    ofm_shape = [1, ifm.shape[1], ifm.shape[2], 1]
    sum_of_exp = Tensor(ofm_shape, DataType.int32, op.name + "_tens0")
    sum_of_exp.quantization = quantization
    op.set_output_tensor(sum_of_exp)
    op.set_ifm_ofm_shapes()
    return op


def create_add(
    name: str,
    ifm: Tensor,
    ifm2: Tensor,
    quantization: QuantizationParameters,
    activation: Optional[ActivationFunction] = None,
    dtype: Optional[DataType] = None,
    attrs: Optional[dict] = None,
    ifm_shape: Optional[Shape4D] = None,
    ifm2_shape: Optional[Shape4D] = None,
) -> Operation:
    return create_binary_elementwise(
        Op.Add, name, ifm, ifm2, quantization, activation, dtype, attrs, ifm_shape, ifm2_shape
    )


def create_clz(
    name: str,
    ifm: Tensor,
    quantization: QuantizationParameters,
    activation: Optional[ActivationFunction] = None,
    dtype: Optional[DataType] = None,
    attrs: Optional[dict] = None,
    ifm_shape: Optional[Shape4D] = None,
) -> Operation:
    return create_unary_elementwise(Op.CLZ, name, ifm, quantization, activation, dtype, attrs, ifm_shape)


def create_mul(
    name: str,
    ifm: Tensor,
    ifm2: Tensor,
    quantization: QuantizationParameters,
    activation: Optional[ActivationFunction] = None,
    dtype: Optional[DataType] = None,
    attrs: Optional[dict] = None,
    ifm_shape: Optional[Shape4D] = None,
    ifm2_shape: Optional[Shape4D] = None,
) -> Operation:
    return create_binary_elementwise(
        Op.Mul, name, ifm, ifm2, quantization, activation, dtype, attrs, ifm_shape, ifm2_shape
    )


def create_shl(
    name: str,
    ifm: Tensor,
    ifm2: Tensor,
    quantization: QuantizationParameters,
    activation: Optional[ActivationFunction] = None,
    dtype: Optional[DataType] = None,
    attrs: Optional[dict] = None,
    ifm_shape: Optional[Shape4D] = None,
    ifm2_shape: Optional[Shape4D] = None,
) -> Operation:
    return create_binary_elementwise(
        Op.SHL, name, ifm, ifm2, quantization, activation, dtype, attrs, ifm_shape, ifm2_shape
    )


def create_shr(
    name: str,
    ifm: Tensor,
    ifm2: Tensor,
    quantization: QuantizationParameters,
    activation: Optional[ActivationFunction] = None,
    dtype: Optional[DataType] = None,
    attrs: Optional[dict] = None,
    ifm_shape: Optional[Shape4D] = None,
    ifm2_shape: Optional[Shape4D] = None,
) -> Operation:
    return create_binary_elementwise(
        Op.SHR, name, ifm, ifm2, quantization, activation, dtype, attrs, ifm_shape, ifm2_shape
    )


def create_sub(
    name: str,
    ifm: Tensor,
    ifm2: Tensor,
    quantization: QuantizationParameters,
    activation: Optional[ActivationFunction] = None,
    dtype: Optional[DataType] = None,
    attrs: Optional[dict] = None,
    ifm_shape: Optional[Shape4D] = None,
    ifm2_shape: Optional[Shape4D] = None,
) -> Operation:
    return create_binary_elementwise(
        Op.Sub, name, ifm, ifm2, quantization, activation, dtype, attrs, ifm_shape, ifm2_shape
    )


def create_unary_elementwise(
    op_type: Op,
    name: str,
    ifm: Tensor,
    quantization: QuantizationParameters,
    activation: Optional[ActivationFunction] = None,
    dtype: Optional[DataType] = None,
    attrs: Optional[dict] = None,
    ifm_shape: Optional[Shape4D] = None,
) -> Operation:
    return create_binary_elementwise(op_type, name, ifm, None, quantization, activation, dtype, attrs, ifm_shape, None)


def create_binary_elementwise(
    op_type: Op,
    name: str,
    ifm: Tensor,
    ifm2: Optional[Tensor],
    quantization: QuantizationParameters,
    activation: Optional[ActivationFunction] = None,
    dtype: Optional[DataType] = None,
    attrs: Optional[dict] = None,
    ifm_shape: Optional[Shape4D] = None,
    ifm2_shape: Optional[Shape4D] = None,
) -> Operation:
    if ifm_shape is None:
        ifm_shape = Shape4D(ifm.shape)
    op = Operation(op_type, name)
    op.add_input_tensor(ifm)
    op.ifm_shapes.append(ifm_shape)
    if ifm2:
        op.add_input_tensor(ifm2)
        if ifm2_shape is None:
            ifm2_shape = Shape4D(ifm2.shape)
        op.ifm_shapes.append(ifm2_shape)
    op.activation = activation
    if not dtype:
        dtype = ifm.dtype
    if attrs:
        op.attrs.update(attrs)

    if ifm2 is None:
        ofm_shape = ifm_shape
    else:
        in_shape = None if ifm.shape == [] else ifm_shape
        in2_shape = None if ifm2.shape == [] else ifm2_shape
        ofm_shape = ifm_shape if ifm_ifm2_correct_order(in_shape, in2_shape) else ifm2_shape

    ofm = Tensor(ofm_shape.as_list(), dtype, f"{op.name}_tens0")
    ofm.quantization = quantization
    op.set_output_tensor(ofm)
    op.ofm_shapes.append(ofm_shape)
    return op


def get_pad_values_from_input(padding) -> Tuple:
    """Returns top, left, bottom, right padding from input values in a Pad input tensor"""
    return (padding[-3][0], padding[-2][0], padding[-3][1], padding[-2][1])
