import onnxruntime
import onnx
import argparse
import numpy as np
from tools import helper


onnx2np_dtype = {
    0: "float",
    1: "float32",
    2: "uint8",
    3: "int8",
    4: "uint16",
    5: "int16",
    6: "int32",
    7: "int64",
    8: "str",
    9: "bool",
    10: "float16",
    11: "double",
    12: "uint32",
    13: "uint64",
    14: "complex64",
    15: "complex128",
    16: "float",
}


def onnx_model_results(path_a, path_b, total_times=10):
    """using onnxruntime to inference two onnx models' ouputs

    :onnx model paths: two model paths
    :total_times: inference times, default to be 10
    :returns: inference results of two models
    """
    # load model a and model b to runtime
    session_a = onnxruntime.InferenceSession(path_a, None)
    session_b = onnxruntime.InferenceSession(path_b, None)
    outputs_a = session_a.get_outputs()
    outputs_b = session_b.get_outputs()

    # check outputs
    assert len(outputs_a) == len(
        outputs_b
    ), "Two models have different output numbers."
    for i in range(len(outputs_a)):
        out_shape_a, out_shape_b = outputs_a[i].shape, outputs_b[i].shape
        out_shape_a = list(
            map(lambda x: x if isinstance(x, int) else 1, out_shape_a)
        )
        out_shape_b = list(
            map(lambda x: x if isinstance(x, int) else 1, out_shape_b)
        )
        assert (
            out_shape_a == out_shape_b
        ), "Output {} has unmatched shapes".format(i)

    # load onnx graph_a and graph_b, to find the initializer and inputs
    # then compare to remove the items in the inputs which will be initialized
    model_a, model_b = onnx.load(path_a), onnx.load(path_b)
    graph_a, graph_b = model_a.graph, model_b.graph
    inputs_a, inputs_b = graph_a.input, graph_b.input
    init_a, init_b = graph_a.initializer, graph_b.initializer

    # remove initializer from raw inputs
    input_names_a, input_names_b = set([ele.name for ele in inputs_a]), set(
        [ele.name for ele in inputs_b]
    )
    init_names_a, init_names_b = set([ele.name for ele in init_a]), set(
        [ele.name for ele in init_b]
    )
    real_inputs_names_a, real_inputs_names_b = (
        input_names_a - init_names_a,
        input_names_b - init_names_b,
    )

    # prepare and figure out matching of real inputs a and real inputs b
    # try to keep original orders of each inputs
    real_inputs_a, real_inputs_b = [], []
    for item in inputs_a:
        if item.name in real_inputs_names_a:
            real_inputs_a.append(item)
    for item in inputs_b:
        if item.name in real_inputs_names_b:
            real_inputs_b.append(item)

    # suppose there's only one real single input tensor for each model
    # find the real single inputs for model_a and model_b
    real_single_input_a = None
    real_single_input_b = None
    size_a, size_b = 0, 0
    shape_a, shape_b = [], []
    for item_a in real_inputs_a:
        size, shape = helper.find_size_shape_from_value(item_a)
        if size:
            assert (
                real_single_input_a is None
            ), "Multiple inputs of first model, single input expected."
            real_single_input_a = item_a
            size_a, shape_a = size, shape
    for item_b in real_inputs_b:
        size, shape = helper.find_size_shape_from_value(item_b)
        if size:
            assert (
                real_single_input_b is None
            ), "Multiple inputs of second model, single input expected."
            real_single_input_b = item_b
            size_b, shape_b = size, shape
    assert size_a == size_b, "Sizes of two models do not match."

    # construct inputs tensors
    input_data_type_a = real_single_input_a.type.tensor_type.elem_type
    input_data_type_b = real_single_input_b.type.tensor_type.elem_type
    input_data_type_a = onnx2np_dtype[input_data_type_a]
    input_data_type_b = onnx2np_dtype[input_data_type_b]

    # run inference
    times = 0
    results_a = [[] for i in range(len(outputs_a))]
    results_b = [[] for i in range(len(outputs_b))]
    while times < total_times:
        # initialize inputs by random data, default to be uniform
        data = np.random.random(size_a)
        input_a = np.reshape(data, shape_a).astype(input_data_type_a)
        input_b = np.reshape(data, shape_b).astype(input_data_type_b)

        input_dict_a = {}
        input_dict_b = {}
        for item_a in real_inputs_a:
            item_type_a = onnx2np_dtype[item_a.type.tensor_type.elem_type]
            input_dict_a[item_a.name] = (
                np.array([]).astype(item_type_a)
                if item_a.name != real_single_input_a.name
                else input_a
            )
        for item_b in real_inputs_b:
            item_type_b = onnx2np_dtype[item_b.type.tensor_type.elem_type]
            input_dict_b[item_b.name] = (
                np.array([]).astype(item_type_b)
                if item_b.name != real_single_input_b.name
                else input_b
            )

        ra = session_a.run([], input_dict_a)
        rb = session_b.run([], input_dict_b)
        for i in range(len(outputs_a)):
            results_a[i].append(ra[i])
            results_b[i].append(rb[i])
        times += 1

    return results_a, results_b


if __name__ == "__main__":
    # Argument parser.
    parser = argparse.ArgumentParser(
        description="Compare two ONNX models to check if "
                    "they have the same output."
    )
    parser.add_argument("in_file_a", help="input ONNX file a")
    parser.add_argument("in_file_b", help="input ONNX file b")

    args = parser.parse_args()

    results_a, results_b = onnx_model_results(
        args.in_file_a, args.in_file_b, total_times=10
    )
    ra_flat = helper.flatten_with_depth(results_a, 0)
    rb_flat = helper.flatten_with_depth(results_b, 0)
    shape_a = [item[1] for item in ra_flat]
    shape_b = [item[1] for item in rb_flat]
    assert shape_a == shape_b, "two results data shape doesn't match"
    ra_raw = [item[0] for item in ra_flat]
    rb_raw = [item[0] for item in rb_flat]

    try:
        np.testing.assert_almost_equal(ra_raw, rb_raw, 4)
        print("Two models have the same behaviour.")
    except Exception as mismatch:
        print(mismatch)
        exit(1)
