#!/usr/bin/python
import os
import shutil
import sys
import onnx
import onnxruntime as ort
import numpy as np

def makeDirForPath(filename):
    if filename.find('/') < 0:
        return
    names = filename.split('/')
    dirname = ""
    for l in range(0, len(names)-1):
        dirname = dirname + names[l] + '/'
    print(dirname)
    if os.path.exists(dirname):
        return
    os.makedirs(dirname)

# a class to get idom for graph
class IDominate:
    def __init__(self, n):
        self.N = n+1
        self.E = 0
        self.dfs_id = 0
        self.__data__ = [[0 for i in range(self.N)] for i in range(7)]
        for i in range(2):
            self.__data__.append([0 for i in range(self.N*10)])
        for i in range(3):
            self.__data__.append([i for i in range(self.N)])
        self.forward = self.__data__[0]
        self.backward = self.__data__[1]
        self.dfn = self.__data__[2]
        self.id = self.__data__[3]
        self.father = self.__data__[4]
        self.dom = self.__data__[5]
        self.idom = self.__data__[6]
        self.to = self.__data__[7]
        self.next = self.__data__[8]
        self.union_find = self.__data__[9]
        self.sdom = self.__data__[10]
        self.best = self.__data__[11]

    def __addedge(self, graph, _from, _to):
        self.E += 1
        self.next[self.E] = graph[_from]
        self.to[self.E] = _to
        graph[_from] = self.E
    # using union_find to compress path
    def __push_union_find(self, v):
        if v == self.union_find[v]:
            return v
        y = self.__push_union_find(self.union_find[v])
        if self.dfn[self.sdom[self.best[self.union_find[v]]]] < self.dfn[self.sdom[self.best[v]]]:
            self.best[v] = self.best[self.union_find[v]]
        self.union_find[v] = y
        return y
    # dfs get dfs_id
    def __dfs(self, s):
        self.dfs_id += 1
        self.dfn[s] = self.dfs_id
        self.id[self.dfs_id] = s
        t = self.forward[s]
        while t:
            if not self.dfn[self.to[t]]:
                self.__dfs(self.to[t])
                self.father[self.to[t]] = s
            t = self.next[t]
    # Lengauer-Tarjan Algorithm: A fast algorithm for finding dominators in a flowgraph
    # ref : https://dl.acm.org/doi/10.1145/357062.357071 
    def __tarjan(self):
        for i in range(self.dfs_id, 1, -1):
            u = self.id[i]
            j = self.backward[u]
            while j:
                if not self.dfn[self.to[j]]:
                    j = self.next[j]
                    continue
                self.__push_union_find(self.to[j])
                if self.dfn[self.sdom[self.best[self.to[j]]]] < self.dfn[self.sdom[u]]:
                    self.sdom[u] = self.sdom[self.best[self.to[j]]]
                j = self.next[j]
            self.__addedge(self.dom, self.sdom[u], u)
            self.union_find[u] = self.father[u]
            u = self.id[i-1]
            j = self.dom[u]
            while j:
                self.__push_union_find(self.to[j])
                if self.sdom[self.best[self.to[j]]] == u:
                    self.idom[self.to[j]] = u
                else:
                    self.idom[self.to[j]] = self.best[self.to[j]]
                j = self.next[j]
        for i in range(2, self.dfs_id+1):
            u = self.id[i]
            if self.idom[u] != self.sdom[u]:
                self.idom[u] = self.idom[self.idom[u]]
    def addEdge(self, _from, _to):
        self.__addedge(self.forward, _from, _to)
        self.__addedge(self.backward, _to, _from)
    def getIDoms(self):
        self.__dfs(1)
        self.__tarjan()
        return self.idom

class TestModel():
    def __copy_to_here(self, modelName):
        newModel = os.path.join('onnx', 'test.onnx')
        try:
            os.mkdir("onnx")
        except:
            print('Dir exist')
        shutil.copyfile(modelName, newModel)
        self.modelName = newModel
        self.model = onnx.load(self.modelName)
        self.outputs = [output.name for output in self.model.graph.output]
    def __init__(self, modelName):
        self.__copy_to_here(modelName)
    def __run_mnn(self):
        mnnconvert_name = 'MNNConvert.exe' if os.name == 'nt' else './MNNConvert'
        if not os.path.exists(mnnconvert_name):
            print("./MNNConvert not exist in this path. Use pymnn instead of C++ to test")
            mnnconvert_name = 'mnnconvert'
        convert = mnnconvert_name + ' -f ONNX --bizCode MNN --modelFile onnx/test.onnx --MNNModel convert_cache.mnn --keepInputFormat=1 --testdir onnx'
        result = os.popen(convert).read()
        print(result)
        return result
    def __run_onnx(self):
        jsonDict = {}
        jsonDict['inputs'] = []
        jsonDict['outputs'] = []
        inputs = {}
        print(self.modelName)
        ort_session = ort.InferenceSession(self.modelName)
        for inputVar in ort_session.get_inputs():
            inp = {}
            inp['name'] = inputVar.name
            shapes = inputVar.shape
            for i in range(0, len(shapes)):
                if type(shapes[i]) == str:
                    shapes[i] = 1
            inp['shape'] = shapes
            print(inputVar.type)
            if inputVar.type.find("int64") >= 0:
                inputs[inputVar.name] = np.random.uniform(0, 12, shapes).astype(np.int64)
            elif inputVar.type.find("int32") >=0:
                inputs[inputVar.name] = np.random.uniform(0, 12, shapes).astype(np.int32)
            elif inputVar.type.find('bool') >=0:
                inputs[inputVar.name] = np.random.uniform(0, 1, shapes).astype(np.bool_)
            else:
                # Float
                inputs[inputVar.name] = np.random.uniform(0.1, 1.2, shapes).astype(np.float32)
            if inputVar.type.find("float16") >= 0:
                inputs[inputVar.name] = inputs[inputVar.name].astype(np.float16)
            jsonDict['inputs'].append(inp)
        print([output.name for output in self.model.graph.output])
        for output in self.model.graph.output:
            jsonDict['outputs'].append(output.name)

        import json
        jsonString = json.dumps(jsonDict, indent=4)
        with open('onnx/input.json', 'w') as f:
            f.write(jsonString)

        print('inputs:')
        for key in inputs:
            print(key)
            path = "onnx/" + key + '.txt'
            makeDirForPath(path)
            f = open(path, 'w')
            np.savetxt(f, inputs[key].flatten())
            f.close()
        outputs = ort_session.run(None, inputs)
        print('outputs:')
        for i in range(0, len(outputs)):
            outputName = self.model.graph.output[i].name
            name = 'onnx/' + outputName + '.txt'
            print(name, outputs[i].shape)
            makeDirForPath(name)
            f = open(name, 'w')
            np.savetxt(f, outputs[i].flatten())
            f.close()
    def __test_specify_output(self, specify_output_name):
        while len(self.model.graph.output) > 0:
            self.model.graph.output.pop()
        if isinstance(specify_output_name, list):
            print(specify_output_name)
            for specify_output_name_enum in specify_output_name:
                new_output = onnx.helper.ValueInfoProto()
                new_output.name = specify_output_name_enum
                self.model.graph.output.append(new_output)
        else:
            new_output = onnx.helper.ValueInfoProto()
            new_output.name = specify_output_name
            self.model.graph.output.append(new_output)
        onnx.save(self.model, self.modelName)
        res = self.Test()
        is_right = ('TEST_SUCCESS' in res or 'Can\'t find var' in res)
        if hasattr(self, 'output_map'):
            print('Test Node :', self.output_map[specify_output_name], is_right)
        return is_right
    def __build_graph(self):
        n = len(self.model.graph.node)
        self.nodes = [node.name for node in self.model.graph.node]
        self.nodes.insert(0, '__null__')
        self.output_map = {}
        self.node_map = {}
        self.node_output = {}
        self.node_input = {}
        idom = IDominate(n); idx = 0
        for node in self.model.graph.node:
            idx += 1
            self.node_map[node.name] = idx
            self.node_output[node.name] = []
            self.node_input[node.name] = []
            for output in node.output:
                self.output_map[output] = node.name
                self.node_output[node.name].append(output)
            for input in node.input:
                if self.output_map.__contains__(input):
                    self.node_input[node.name].append(input)
                    idom.addEdge(self.node_map[self.output_map[input]], self.node_map[node.name])
        self.idoms = idom.getIDoms()
        #print(self.idoms)
    def __get_dom_path(self, left_id, right_id):
        path = []
        while left_id != right_id:
            path.insert(0, right_id)
            right_id = self.idoms[right_id]
        path.insert(0, left_id)
        return path
    def __sub_graph(self, last_right_id, first_error_id):
        while last_right_id == self.idoms[first_error_id]:
            right_input = 0
            inputs = self.node_input[self.nodes[first_error_id]]
            for input in inputs:
                if not self.__test_specify_output(input):
                    first_error_id = self.node_map[self.output_map[input]]
                    break
                else:
                    right_input += 1
            if right_input == len(inputs):
                break
        return first_error_id
    def __binary_search(self, left_id, right_id):
        dom_path = self.__get_dom_path(left_id, right_id)
        left = 0; right = len(dom_path) - 1
        while left < right - 1:
            middle = left + (right - left) // 2
            test_node = self.nodes[dom_path[middle]]
            if self.__test_specify_output(self.node_output[test_node][0]):
                left = middle
            else:
                right = middle
        last_right_node = self.nodes[dom_path[left]]
        first_error_node = self.nodes[dom_path[right]]
        last_right_id = self.node_map[last_right_node]
        first_error_id = self.node_map[first_error_node]
        print('Error is between ', last_right_node, ' and ', first_error_node)
        new_first_error_id = -1
        if last_right_id < first_error_id - 1:
            new_first_error_id = self.__sub_graph(last_right_id, first_error_id)
            if self.idoms[new_first_error_id] != last_right_id:
                self.__binary_search(last_right_id, new_first_error_id)
                return
        if new_first_error_id > 0:
            print('### First Error Node is : ', self.nodes[new_first_error_id])
    def TestName(self, name):
        self.__test_specify_output(name)
    def Test(self):
        self.__run_onnx()
        res = self.__run_mnn()
        return res
    def Debug(self):
        self.__build_graph()
        left_id = self.node_map[self.nodes[1]]
        right_id = self.node_map[self.output_map[self.outputs[0]]]
        self.__binary_search(left_id, right_id)

if __name__ == '__main__':
    modelName = sys.argv[1]
    t = TestModel(modelName)
    if len(sys.argv) > 2:
        if sys.argv[2] == 'DEBUG':
            message = t.Test()
            print(message)
            if message.find("TEST_SUCCESS") < 0:
                debugMode = len(sys.argv) > 2
                print('Debug Mode: ', debugMode)
                t.Debug()
        else:
            names = []
            for i in range(2, len(sys.argv)):
                names.append(sys.argv[i])
            t.TestName(names)
    else:
        t.Test()
