//
//  TFGraphResolver.cpp
//  MNNConverter
//
//  Created by MNN on 2020/06/13.
//  Copyright © 2018, Alibaba Group Holding Limited
//

#include "TFGraphResolver.hpp"
#include "TFGraphResolverHelpers.hpp"

#include <vector>
#include <queue>
#include <unordered_map>
#include <unordered_set>

#include "graph.pb.h"
#include "TmpGraph.hpp"
#include "tfOpConverter.hpp"
#include "MNN_generated.h"
#include "../compression/quantization.hpp"
#include <flatbuffers/util.h>

void TFGraph::AddNode(const NodeDef* node) {
    std::unique_ptr<TFNode> tf_node(new TFNode);
    tf_node->node_def = node;
    tf_node->name = node->name();
    tf_node->op = node->op();
    nodes_.push_back(std::move(tf_node));
}

void TFGraph::Finalize() {
    std::unordered_map<std::string, TFNode*> nodes;
    for (auto& node : nodes_) {
        nodes.emplace(node->name, node.get());
    }
    for (auto& node : nodes_) {
        const NodeDef* node_def = node->node_def;
        for (int i = 0; i < node_def->input_size(); ++i) {
            const std::string& input = node_def->input(i);
            if (IsControlInput(input)) {
                continue;
            }
            std::string input_op = input;
            auto splits = RSplitString(input, ":");
            if (splits.size() == 2) {
                input_op = splits.at(0);
            }
            TFNode* start = nodes.at(input_op);
            std::unique_ptr<TFEdge> edge(new TFEdge);
            *edge = TFEdge{input, start, node.get()};
            node->inputs.push_back(edge.get());
            start->outputs.push_back(edge.get());
            edges_.push_back(std::move(edge));
        }
    }
    for (auto& node : nodes_) {
        if (node->outputs.empty()) {
            final_nodes_.push_back(node.get());
        }
    }
}

std::unique_ptr<MNN::SubGraphProtoT> TFGraph::ToProto() const {
    std::unique_ptr<MNN::SubGraphProtoT> graph_proto(new MNN::SubGraphProtoT);
    graph_proto->name = name_;
    std::vector<const TFNode*> entry_nodes;

    std::unordered_map<std::string, int> tensor_indices;
    // Add normal nodes.
    for (int i = 0; i < nodes_.size(); ++i) {
        TFNode* node = nodes_[i].get();
        std::shared_ptr<TmpNode> tempNode(new TmpNode());
        tempNode->opName = node->name;
        tempNode->opType = node->op;
        tempNode->tfNode = node->node_def;

        MNN::OpT *op = new MNN::OpT;
        auto creator = tfOpConverterSuit::get()->search(tempNode->opType);
        DCHECK(creator) << "MNN Converter NOT_SUPPORTED_OP: [ "
                        << tempNode->opType << " ]";
        op->name = tempNode->opName;
        op->type = creator->opType();
        op->main.type = creator->type();

        // resize the inputIndexes and outputIndexes
        int input_size = node->inputs.size();
        op->inputIndexes.resize(input_size);

        // -1 is placeholder value, and the number of -1 is the number of
        // output tensors.
        // defalut: every op output one tensor, if the number of the output
        // tensors is bigger than 1, set the outputIndexes in the op
        // converter(void run(MNN::OpT *dstOp, TmpNode *srcNode))
        op->outputIndexes = {-1};
        creator->run(op, tempNode.get());

        for (int j = 0; j < input_size; j++) {
            std::string input = node->inputs[j]->name;
            auto it = tensor_indices.find(input);
            if (it == tensor_indices.end()) {
                int index = tensor_indices.size();
                it = tensor_indices.emplace(input, index).first;
                graph_proto->tensors.push_back(input);
            }
            op->inputIndexes[j] = it->second;
        }

        int output_size = node->outputs.size();
        for (int j = 0; j < node->outputs.size(); ++j) {
            std::string output = node->outputs[j]->name;
            auto it = tensor_indices.find(output);
            if (it == tensor_indices.end()) {
                int index = tensor_indices.size();
                it = tensor_indices.emplace(output, index).first;
                graph_proto->tensors.push_back(output);
            }
            int index = 0;
            auto splits = RSplitString(output, ":");
            if (splits.size() == 2) {
                index = atoi(splits[1].c_str());
            }
            if (op->outputIndexes.size() <= index) {
                int origin_size = op->outputIndexes.size();
                op->outputIndexes.resize(index + 1);
                for (int p = origin_size; p <= index; ++p) {
                    op->outputIndexes[p] = -1;
                }
            }
            op->outputIndexes[index] = it->second;
        }
        graph_proto->nodes.emplace_back(op);
    }

    for (auto &op : graph_proto->nodes) {
        for (int i = 0; i < op->outputIndexes.size(); ++i) {
            if (op->outputIndexes[i] == -1) {
                int index = graph_proto->tensors.size();
                op->outputIndexes[i] = index;
                std::string output = op->name;
                if (i != 0) {
                    output += ":" + flatbuffers::NumToString(i);
                }
                graph_proto->tensors.emplace_back(output);
            }
        }
    }
    return std::move(graph_proto);
}

std::unique_ptr<TFEdge> TFGraphResolver::BuildEdge(
        const std::string& name, TFNode* start, TFNode* end) {
    std::unique_ptr<TFEdge> edge(new TFEdge);
    *edge = TFEdge{name, start, end};
    return std::move(edge);
}

std::unique_ptr<TFNode> TFGraphResolver::BuildQuantOrDequantNode(
                            const std::string& name,
                            const std::string& op,
                            const int& nbit,
                            const std::vector<float>& scales,
                            const float& zero_point, const float& clamp_min, const float& clamp_max,
                            const MNN::Compression::LayerQuantizeParams_QuantMethod& method) {
    std::unique_ptr<NodeDef> node_def(new NodeDef);
    *(node_def->mutable_name()) = name;
    *(node_def->mutable_op()) = op;
    (*node_def->mutable_attr())["nbit"].set_i(nbit);
    auto* list = (*node_def->mutable_attr())["scale"].mutable_list();
    for (int i = 0; i < scales.size(); ++i) {
        if (op == "CustomQuantize") {
            list->mutable_f()->Add(1.f / scales[i]);
        } else {
            list->mutable_f()->Add(scales[i]);
        }
    }
    (*node_def->mutable_attr())["zero_point"].set_f(zero_point);
    (*node_def->mutable_attr())["clamp_min"].set_f(clamp_min);
    (*node_def->mutable_attr())["clamp_max"].set_f(clamp_max);
    (*node_def->mutable_attr())["method"].set_i(int(method));
    std::unique_ptr<TFNode> quant_node(new TFNode);
    quant_node->name = name;
    quant_node->op = op;
    quant_node->node_def = node_def.get();

    main_graph()->allocated_nodes_.push_back(std::move(node_def));
    return std::move(quant_node);
}

void TFGraphResolver::ResolveQuantization(
        TFGraph* graph,
        const compression::Quantization& int8_calibration) {
    std::vector<std::unique_ptr<TFNode>> append_nodes;
    std::vector<std::unique_ptr<TFEdge>> append_edges;

    static int64_t uuid = 0;
    auto AddQuantizeAndDequantizeNodes =
            [&, this](const std::vector<TFEdge*> edges,
                      const compression::Quantization::TensorParams& params) {
        TFNode* start_node = edges.at(0)->start;
        for (TFEdge* edge : edges) {
            EraseOutput(start_node, edge);
        }
        auto splits = RSplitString(edges.at(0)->name, ":");
        const std::string& op_name = splits.at(0);
        // Add quantize node.
        std::string quant_name = op_name + "_quant_" + flatbuffers::NumToString(uuid);
        std::unique_ptr<TFNode> quant_node = BuildQuantOrDequantNode(
            quant_name, "CustomQuantize", params.nbit, params.scale,
            params.zero_point, params.clamp_min, params.clamp_max, params.method);
        // Add dequantize node.
        std::string dequant_name = quant_name + "_dequant_" + flatbuffers::NumToString(uuid);
        std::unique_ptr<TFNode> dequant_node = BuildQuantOrDequantNode(
            dequant_name, "CustomDequantize", params.nbit, params.scale,
            params.zero_point, params.clamp_min, params.clamp_max, params.method);

        // Update UUID.
        ++uuid;

        // Connect quantize and dequantize node.
        std::unique_ptr<TFEdge> quant_edge =
            BuildEdge(edges.at(0)->name, start_node, quant_node.get());
        // Connect dequantize and the next node.
        std::unique_ptr<TFEdge> dequant_edge =
            BuildEdge(quant_node->name, quant_node.get(), dequant_node.get());

        AddOutput(start_node, quant_edge.get());

        quant_node->inputs = {quant_edge.get()};
        quant_node->outputs = {dequant_edge.get()};
        dequant_node->inputs = {dequant_edge.get()};
        dequant_node->outputs = edges;
        for (TFEdge* edge : edges) {
            edge->name = dequant_node->name;
            edge->start = dequant_node.get();
        }
        append_nodes.push_back(std::move(quant_node));
        append_nodes.push_back(std::move(dequant_node));
        append_edges.push_back(std::move(quant_edge));
        append_edges.push_back(std::move(dequant_edge));

        // Return dequant edge.
        return append_edges.back().get();
    };

    const auto& tensor_params = int8_calibration.tensors;
    for (auto& node : graph->nodes_) {
        std::unordered_map<std::string, std::vector<TFEdge*>> quant_edges;
        for (TFEdge* output : node->outputs) {
            std::string tensor_name = output->name;
            if (node->op == "Enter" || node->op == "Switch") {
                // The input names of the node maybe replaced by the quantize
                // and dequantize op, so here we use the input name from the
                // `node_def` since it should not be modified at any time.
                // tensor_name = node->inputs.at(0)->name;
                tensor_name = node->node_def->input(0);
            }
            quant_edges[tensor_name].push_back(output);
        }
        for (const auto& it : quant_edges) {
            auto p = tensor_params.find(it.first);
            if (p == tensor_params.end()) {
                continue;
            }
            const auto& params = p->second.at(0);
            AddQuantizeAndDequantizeNodes(it.second, params);
        }
    }
    for (auto& node : graph->nodes_) {
        std::unordered_map<std::string, std::vector<TFEdge*>> quant_edges;
        for (int i = 0; i < node->inputs.size(); ++i) {
            TFEdge* edge = node->inputs[i];
            quant_edges[edge->name].push_back(edge);
        }
        for (const auto& it : quant_edges) {
            auto p = tensor_params.find(it.first);
            if (p == tensor_params.end()) {
                continue;
            }
            const auto& params = p->second.at(0);
            AddQuantizeAndDequantizeNodes(it.second, params);
        }
    }
    // Append nodes and edges to root graph.
    for (auto& node : append_nodes) {
        main_graph()->nodes_.push_back(std::move(node));
    }
    for (auto& edge : append_edges) {
        main_graph()->edges_.push_back(std::move(edge));
    }
}

TFGraphResolver::TFGraphResolver(const tensorflow::GraphDef& graph_def) {
    std::unique_ptr<TFGraph> tf_graph(new TFGraph);
    const int count = graph_def.node_size();
    for (int i = 0; i < count; ++i) {
        const NodeDef& node_def = graph_def.node(i);
        tf_graph->AddNode(&node_def);
    }
    tf_graph->Finalize();
    graphs_.push_back(std::move(tf_graph));

    TFGraph* main_graph = graphs_.back().get();
}

const TFGraph* TFGraphResolver::graph(const int graph_index) const {
    return graphs_.at(graph_index).get();
}

TFGraph* TFGraphResolver::graph(const int graph_index) {
    return graphs_.at(graph_index).get();
}

TFGraph* TFGraphResolver::main_graph() {
    return this->graph(0);
}
