#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# 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
#
# http://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.
#


import sys
import tensorflow as tf
from tensorflow_quantization import QuantizationSpec
from tensorflow_quantization.quantize import LayerConfig
from onnx_graph_qdq_validator import validate_quantized_model
from tensorflow_quantization.utils import CreateAssetsFolders
from network_pool import (
    otho_28_28,
    lotho_28_28,
    lobelia_28_28,
    merry_28_28,
    pippin_28_28,
)
import pytest

# Create a directory to save wrapper test data
test_assets = CreateAssetsFolders("test_quantize_wrappers")


# ###################################################
# ####### Conv2D layer wrapper tests ################
# ###################################################


def test_conv2d_wrapper_quant_full_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = otho_28_28()

    # Quantization
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="conv_0"),
        LayerConfig(name="conv_1"),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_conv2d_wrapper_quant_partial_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = otho_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(name="conv_1")
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="conv_1"),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode="partial", qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_conv2d_wrapper_quant_partial_only_input_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = otho_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(name="conv_0", quantize_weight=False)
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="conv_0", quantize_weight=False),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode="partial", qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_conv2d_wrapper_quant_partial_only_weight_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = otho_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(name="conv_0", quantize_input=False)
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="conv_0", quantize_input=False),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode="partial", qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


# ###################################################
# ####### DepthwiseConv2D layer wrapper tests #######
# ###################################################


def test_depthwise_conv2d_wrapper_quant_full_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = lotho_28_28()

    # Quantization
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="dconv_0"),
        LayerConfig(name="dconv_1"),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_depthwise_conv2d_wrapper_quant_partial_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = lotho_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(name="dconv_1")
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="dconv_1"),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode="partial", qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_depthwise_conv2d_wrapper_quant_partial_only_input_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = lotho_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(name="dconv_1", quantize_weight=False)
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="dconv_1", quantize_weight=False),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode="partial", qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_depthwise_conv2d_wrapper_quant_partial_only_weight_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = lotho_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(name="dconv_1", quantize_input=False)
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="dconv_1", quantize_input=False),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode="partial", qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


# ###################################################
# ####### Dense layer wrapper tests #################
# ###################################################


def test_dense_wrapper_quant_full_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = lobelia_28_28()

    # Quantization
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="dense_0"),
        LayerConfig(name="dense_1"),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_dense_wrapper_quant_partial_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = lobelia_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(name="dense_0")
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="dense_0"),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode="partial", qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_dense_wrapper_quant_partial_only_input_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = lobelia_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(name="dense_0", quantize_weight=False)
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="dense_0", quantize_weight=False),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode="partial", qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_dense_wrapper_quant_partial_only_weight_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = lobelia_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(name="dense_1", quantize_input=False)
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="dense_1", quantize_input=False),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode="partial", qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"

    tf.keras.backend.clear_session()


# ###################################################
# ####### Concatenation layer wrapper tests #########
# ###################################################


def test_concat_wrapper_quant_full_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = merry_28_28()

    # Quantization
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="conv2d"),
        LayerConfig(name="conv2d_1"),
        LayerConfig(name="conv2d_3"),
        LayerConfig(name="conv2d_4"),
        LayerConfig(name="conv2d_2"),
        LayerConfig(name="dense"),
        LayerConfig(name="dense_1"),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_concat_wrapper_quant_full_quant_bn_concat_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = merry_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(name="batch_normalization_3")
    qspec.add(name="concatenate", quantization_index=[0, 1])
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="conv2d"),
        LayerConfig(name="conv2d_1"),
        LayerConfig(name="conv2d_3"),
        LayerConfig(name="conv2d_4"),
        LayerConfig(name="batch_normalization_3"),
        LayerConfig(name="conv2d_2"),
        LayerConfig(name="concatenate", quantization_index=[0, 1]),
        LayerConfig(name="dense"),
        LayerConfig(name="dense_1"),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_concat_wrapper_quant_specific_index_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = merry_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(
        name="concatenate",
        quantize_input=True,
        quantize_weight=False,
        quantization_index=[0],
    )
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="concatenate", quantization_index=[0]),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode="partial", qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


# ###################################################
# ####### Add layer wrapper tests ###################
# ###################################################


# Use KerasModelLayersSurgeon() from utils to find layer names.
def test_add_wrapper_quant_full_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = pippin_28_28()

    # Quantization
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="conv2d"),
        LayerConfig(name="conv2d_1"),
        LayerConfig(name="conv2d_2"),
        LayerConfig(name="conv2d_3"),
        LayerConfig(name="conv2d_4"),
        LayerConfig(name="conv2d_6"),
        LayerConfig(name="conv2d_5"),
        LayerConfig(name="dense"),
        LayerConfig(name="dense_1"),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_add_wrapper_quant_partial_specific_index_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = pippin_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(
        name="add", quantize_input=True, quantize_weight=False, quantization_index=[1]
    )
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="add", quantization_index=[1])
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode="partial", qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


def test_add_wrapper_quant_full_specific_index_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = pippin_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(
        name="add", quantize_input=True, quantize_weight=False, quantization_index=[1]
    )
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="conv2d"),
        LayerConfig(name="conv2d_1"),
        LayerConfig(name="conv2d_2"),
        LayerConfig(name="conv2d_3"),
        LayerConfig(name="add", quantization_index=[1]),
        LayerConfig(name="conv2d_4"),
        LayerConfig(name="conv2d_6"),
        LayerConfig(name="conv2d_5"),
        LayerConfig(name="dense"),
        LayerConfig(name="dense_1"),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


# ########################################################
# ############ Test subset layer class selection #########
# ########################################################


def test_subset_layer_class_selection_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = pippin_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(name="Conv2D", is_keras_class=True)
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="conv2d"),
        LayerConfig(name="conv2d_1"),
        LayerConfig(name="conv2d_2"),
        LayerConfig(name="conv2d_3"),
        LayerConfig(name="conv2d_4"),
        LayerConfig(name="conv2d_6"),
        LayerConfig(name="conv2d_5"),
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode='partial', qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


# ########################################################
# ############ Test missing layer name warning ###########
# ########################################################


def test_missing_layer_name_warning_nv():
    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = pippin_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(
        name="add", quantize_input=True, quantize_weight=False, quantization_index=[1]
    )
    qspec.add(name="wrong_layer")
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="add", quantization_index=[1])
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode='partial', qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()


# ########################################################
# ##### Test Add,Concat out of range index warning #######
# ########################################################


@pytest.mark.skip(
    reason="When quantization index out of range does not give error but still wraps \
add layer without quantizing any input"
)
def test_out_of_range_index():

    # Create experiment specific directory
    this_function_name = sys._getframe().f_code.co_name

    # Baseline model
    nn_model = pippin_28_28()

    # Quantization
    qspec = QuantizationSpec()
    qspec.add(
        name="add", quantize_input=True, quantize_weight=False, quantization_index=[3]
    )
    qspec.add(name="dense")
    # (Optional) QDQ node placement check
    #   Here just to explicitly show the user which layers are quantized.
    expected_qdq_insertion = [
        LayerConfig(name="add"), LayerConfig(name="dense")
    ]
    q_model, vr = validate_quantized_model(
        test_assets, nn_model, quantization_mode='partial', qspec=qspec, test_name=this_function_name,
        expected_qdq_insertion=expected_qdq_insertion
    )

    assert vr, "ONNX QDQ Validation failed!"
    tf.keras.backend.clear_session()
