//
//  Transformer.cpp
//  MNN
//
//  Created by MNN on 2019/12/16.
//  Copyright © 2018, Alibaba Group Holding Limited
//

#include "Transformer.hpp"
#include "OpConverter.hpp"
#include "MNN_generated.h"
#include <MNN/expr/ExprCreator.hpp>
using namespace MNN::Express;
namespace MNN {
namespace Train {

bool TurnTrainable::onExecute(const std::vector<VARP>& outputs, std::shared_ptr<Parameters> p) {
    auto exprs = Variable::getExecuteOrder(outputs);
    {
        auto isTraining = _Input({}, NCHW, halide_type_of<int>());
        isTraining->setName("is_training");
        trainInfo["is_training"] = isTraining;
        isTraining = _Cast<float>(isTraining);
        isTraining->setName("is_training_float");
        trainInfo["is_training_float"] = isTraining;
        trainInfo["one_float"] = _Scalar<float>(1.0f);
        trainInfo["bn_momentum"] = _Scalar<float>(mConfig.extraParams["BatchNorm"]["momentum"]->f);
        // Turn convolution be trainable convolution
        for (auto expr : exprs) {
            auto newExpr = OpConverter::convert(expr, trainInfo);
            if (newExpr.get() != expr.get()) {
                Expr::replace(expr, newExpr);
            }
        }
    }
    exprs                = Variable::getExecuteOrder(outputs);
    auto& noUpdateOps = mConfig.noUpdateOps;
    auto& onlyUpdateOps = mConfig.onlyUpdateOps;
    // Collect Const Variable and turn to Trainable
    for (auto v : exprs) {
        if (v->get() == nullptr && VARP::INPUT != v->inputType()) {
            auto name = v->name();
            auto info = v->outputInfo(0);
            if (halide_type_float != info->type.code) {
                continue;
            }

            bool update;
            if (!onlyUpdateOps.empty()) {
                update = false;
                for (auto limit : onlyUpdateOps) {
                    if (name.find(limit) != std::string::npos) {
                        update = true;
                        break;
                    }
                }
            } else {
                update = true;
                for (auto limit : noUpdateOps) {
                    if (name.find(limit) != std::string::npos) {
                        update = false;
                        break;
                    }
                }
            }
            
            auto va = Variable::create(v, 0);
            if (update && name != "") {
                MNN_PRINT("Add Variable: %s\n", name.c_str());
                va.fix(VARP::TRAINABLE);
                if (name.find("Weight") == std::string::npos && name.find("Bias") == std::string::npos) {
                    MNN_PRINT(">>>\ncheck mnn model if const '%s' is a learnable parameter in your original training model, ", name.c_str());
                    MNN_PRINT("if not, add it to transformConfig.json NoUpdateOps\n<<<\n");
                }
            } else {
                va.fix(VARP::CONSTANT);
            }
        }
    }
    return true;
}

std::shared_ptr<Express::Optimizer> Transformer::turnModelToTrainable(TrainConfig config) {
    std::shared_ptr<Express::Optimizer> res;
    res.reset(new TurnTrainable(std::move(config)));
    return res;
}

bool InferOptimizer::onExecute(const std::vector<VARP>& outputs, std::shared_ptr<Parameters> parameters) {
    auto exprs = Variable::getExecuteOrder(outputs);

    // convert trainable to const
    for (auto& expr : exprs) {
        if (expr->inputs().size() == 0 && expr->inputType() == VARP::InputType::TRAINABLE) {
            auto newConst = Variable::create(expr);
            newConst.fix(VARP::InputType::CONSTANT);
            newConst->setName(expr->name());
            auto newExpr = newConst->expr().first;
            newExpr->setName(expr->name());
            Expr::replace(expr, newExpr);
        }
    }

    // merge bn after conv into conv
    // convert single bn to scale
    std::set<std::string> bnNames;
    std::string pattern1 = "_MNN_BN_after_conv_first_op";
    std::string pattern2 = "_MNN_single_BN_first_op";
    for (auto& expr : exprs) {
        if (expr->name().find(pattern1) != std::string::npos) {
            std::string bnName = expr->name();
            for (int i = 0; i < pattern1.size(); i++) {
                bnName.pop_back();
            }
            bnNames.insert(bnName);
        }
        if (expr->name().find(pattern2) != std::string::npos) {
            std::string bnName = expr->name();
            for (int i = 0; i < pattern2.size(); i++) {
                bnName.pop_back();
            }
            bnNames.insert(bnName);
        }
    }

    std::map<std::string, std::map<std::string, EXPRP>> bnInfo;
    for (auto& name : bnNames) {
        for (auto& expr : exprs) {
            auto inputs = expr->inputs();
            if (expr->name() == name) {
                bnInfo[name]["Self"] = expr;
            }
            if (inputs.size() == 0 && expr->name() == name + "_BN_RunningMean_Weight") {
                bnInfo[name]["RunningMean"] = expr;
            }
            if (inputs.size() == 0 && expr->name() == name + "_BN_RunningVariance_Weight") {
                bnInfo[name]["RunningVariance"] = expr;
            }
            if (inputs.size() == 0 && expr->name() == name + "_BN_Gamma_Weight") {
                bnInfo[name]["Gamma"] = expr;
            }
            if (inputs.size() == 0 && expr->name() == name + "_BN_Beta_Bias") {
                bnInfo[name]["Bias"] = expr;
            }
            if (inputs.size() == 0 && expr->name() == name + "_BN_Eps_Weight") {
                bnInfo[name]["Eps"] = expr;
            }
            if (expr->name() == name + pattern1) {
                bnInfo[name]["FirstOpAfterConv"] = expr;
            }
            if (expr->name() == name + pattern2) {
                bnInfo[name]["FirstOpSingleBN"] = expr;
            }
        }
    }
    for (auto& bn : bnInfo) {
        auto bnName = bn.first;
        auto info = bn.second;

        bool bnAfterConv = false;
        if (info.find("FirstOpAfterConv") != info.end()) {
            bnAfterConv = true;
        }

        auto rm = _Convert(Variable::create(info["RunningMean"]), NCHW);
        auto rv = _Convert(Variable::create(info["RunningVariance"]), NCHW);
        auto gamma = _Convert(Variable::create(info["Gamma"]), NCHW);
        auto bias = _Convert(Variable::create(info["Bias"]), NCHW);
        auto eps = Variable::create(info["Eps"]);

        auto s = _Sqrt(rv + eps);
        auto alpha = gamma / s;
        auto beta = bias - rm / s * gamma;

        if (bnAfterConv) {
            auto firstOp = info["FirstOpAfterConv"];
            auto convExpr = firstOp->inputs()[0]->expr().first;
            if (convExpr->get() == nullptr || convExpr->get()->type() != OpType_Convolution) {
                continue;
            }
            auto convInput = convExpr->inputs()[0];
            auto w = convExpr->inputs()[1];
            auto b = convExpr->inputs()[2];
            
            auto nw = w * _Reshape(alpha, {b->getInfo()->dim[0], 1, 1, 1});
            nw.fix(w->expr().first->inputType());
            nw->setName(w->name());
            auto nb = _Reshape(alpha, {b->getInfo()->dim}) * b + _Reshape(beta, b->getInfo()->dim);
            nb.fix(b->expr().first->inputType());
            nb->setName(b->name());

            std::vector<VARP> newInputs = {convInput, nw, nb};
            auto newConv = Expr::create(convExpr->extra(), std::move(newInputs));
            Expr::replace(info["Self"], newConv);
        } else {
            auto firstOp = info["FirstOpSingleBN"];
            auto inputs = firstOp->inputs();
            std::vector<float> scales, p;
            for (int i = 0; i < beta->getInfo()->size; i++) {
                scales.push_back(alpha->readMap<float>()[i]);
                p.push_back(beta->readMap<float>()[i]);
            }
            auto res = _Scale(inputs[0], beta->getInfo()->size, std::move(scales), std::move(p));
            res->setName(info["Self"]->name());
            Expr::replace(info["Self"], res->expr().first);
        }
    }

    exprs = Variable::getExecuteOrder(outputs);
    for (auto& iter : exprs) {
        auto op = iter->get();
        if (nullptr == op) {
            continue;
        }
        if (op->type() != OpType_ConvInt8 && op->type() != OpType_DepthwiseConvInt8) {
            continue;
        }
        auto inputExpr = iter->inputs()[0]->expr().first;
        if (inputExpr->get() == nullptr) {
            continue;
        }
        if (inputExpr->get()->type() != OpType_FloatToInt8) {
            continue;
        }
        auto subInputExpr = inputExpr->inputs()[0]->expr().first;
        if (subInputExpr->get() == nullptr) {
            continue;
        }
        if (subInputExpr->get()->type() != OpType_Int8ToFloat) {
            continue;
        }
        //MNN_PRINT("Find direct\n");
        std::vector<VARP> newInputs = subInputExpr->inputs();
        auto newExpr = Expr::create(iter->extra(), std::move(newInputs));
        Expr::replace(iter, newExpr);
    }
    return true;
}

std::shared_ptr<Express::Optimizer> Transformer::turnModelToInfer() {
    return std::shared_ptr<Optimizer>(new InferOptimizer);
}
} // namespace Train
} // namespace MNN
