// Copyright (C) 2018-2019 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//

#include <gtest/gtest.h>
#include <gmock/gmock-spec-builders.h>
#include "mkldnn_plugin/mkldnn_graph.h"

#include "test_graph.hpp"

#include "single_layer_common.hpp"
#include <mkldnn_plugin/mkldnn_extension_utils.h>
#include <extension/ext_list.hpp>
#include "tests_common.hpp"


using namespace ::testing;
using namespace std;
using namespace mkldnn;


struct math_test_params {
    std::string                 math_function;
    InferenceEngine::SizeVector in_out;
    std::vector<float>          input_tensor;
    std::vector<float>          alpha;
    std::vector<float>          beta;
    std::vector<float>          gamma;
    std::vector<float>          reference;

    std::vector<std::function<void(MKLDNNPlugin::PrimitiveDescInfo)>> comp;
};

void ref_math(
    std::string                    math_function,
    InferenceEngine::TBlob<float> &src,
    std::vector<float>             alpha,
    std::vector<float>             beta,
    std::vector<float>             gamma,
    InferenceEngine::TBlob<float> &dst
) {
    size_t i;
    float* src_data = src.data();
    float *dst_data = dst.data();
    size_t dst_size = dst.size();

    if (math_function == "Erf") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = std::erf(src_data[i]);
        }
    } else if (math_function == "Abs") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = (std::abs)(src_data[i]);
        }
    } else if (math_function == "Acos") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = acosf(src_data[i]);
        }
    } else if (math_function == "Acosh") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = acoshf(src_data[i]);
        }
    } else if (math_function == "Asin") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = asinf(src_data[i]);
        }
    } else if (math_function == "Asinh") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = asinhf(src_data[i]);
        }
    } else if (math_function == "Atan") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = atanf(src_data[i]);
        }
    } else if (math_function == "Atanh") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = atanhf(src_data[i]);
        }
    } else if (math_function == "Ceil") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = ceilf(src_data[i]);
        }
    } else if (math_function == "Cos") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = cosf(src_data[i]);
        }
    } else if (math_function == "Cosh") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = coshf(src_data[i]);
        }
    } else if (math_function == "Floor") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = floorf(src_data[i]);
        }
    } else if (math_function == "HardSigmoid") {
        alpha[0] = (alpha[0] == 0.0f) ? 0.2f : alpha[0];
        beta[0] = (beta[0] == 0.0f) ? 0.5f : beta[0];
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = (std::max)(0.f, (std::min)(1.f, alpha[0] * src_data[i] + beta[0]));
        }
    } else if (math_function == "Log") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = logf(src_data[i]);
        }
    } else if (math_function == "Neg") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = -src_data[i];
        }
    } else if (math_function == "Reciprocal") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = 1.0f / src_data[i];
        }
    } else if (math_function == "Selu") {
        alpha[0] = (alpha[0] == 0.0f) ? 1.67326f : alpha[0];
        gamma[0] = (gamma[0] == 0.0f) ? 1.0507f : gamma[0];
        for (i = 0; i < dst_size; i++) {
            float x = src_data[i];
            dst_data[i] = (x > 0.0f) ? (gamma[0] * x) : (gamma[0] * alpha[0] * (exp(x) - 1.0f));
        }
    } else if (math_function == "Sign") {
        for (i = 0; i < dst_size; i++) {
            if (src_data[i] > 0.0f) dst_data[i] = 1.0f;
            else if (src_data[i] < 0.0f) dst_data[i] = -1.0f;
            else dst_data[i] = 0.0f;
        }
    } else if (math_function == "Sin") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = sinf(src_data[i]);
        }
    } else if (math_function == "Sinh") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = sinhf(src_data[i]);
        }
    } else if (math_function == "Softplus") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = logf(expf(src_data[i]) + 1);
        }
    } else if (math_function == "Softsign") {
        for (i = 0; i < dst_size; i++) {
            float x = src_data[i];
            dst_data[i] = x / (1.f + (std::abs)(x));
        }
    } else if (math_function == "Tan") {
        for (i = 0; i < dst_size; i++) {
            dst_data[i] = tanf(src_data[i]);
        }
    }
}

class MKLDNNCPUExtMathTests: public TestsCommon, public WithParamInterface<math_test_params> {
    std::string model_t = R"V0G0N(
<net Name="Math_net" version="2" precision="FP32" batch="1">
    <layers>
        <layer name="Input" type="Input" precision="FP32" id="1">
            <output>
                <port id="1">
                    _IN_OUT_
                </port>
            </output>
        </layer>
        <layer name="math" id="2" type="_MATH_FUNCTION_" precision="FP32">
            <data _ALPHA_ _BETA_ _GAMMA_/>
            <input>
                <port id="1">
                    _IN_OUT_
                </port>
            </input>
            <output>
                <port id="3">
                    _IN_OUT_
                </port>
            </output>
        </layer>
    </layers>
    <edges>
        <edge from-layer="1" from-port="1" to-layer="2" to-port="1"/>
    </edges>
</net>
)V0G0N";

    std::string getModel(math_test_params p) {
        std::string model = model_t;
        std::string in_out = "";
        std::string alpha;
        std::string beta;
        std::string gamma;

        for (auto& dst : p.in_out) {
            in_out += "<dim>";
            in_out += std::to_string(dst) + "</dim>\n";
        }

        REPLACE_WITH_STR(model, "_IN_OUT_", in_out);
        REPLACE_WITH_STR(model, "_MATH_FUNCTION_", p.math_function);

        if (p.alpha.size()) {
            alpha = "alpha=\"" + to_string_c_locale(p.alpha[0]) + "\"";
        }
        REPLACE_WITH_STR(model, "_ALPHA_", alpha);

        if (p.beta.size()) {
            beta = "beta=\"" + to_string_c_locale(p.beta[0]) + "\"";
        }
        REPLACE_WITH_STR(model, "_BETA_", beta);

        if (p.gamma.size()) {
            gamma = "gamma=\"" + to_string_c_locale(p.gamma[0]) + "\"";
        }
        REPLACE_WITH_STR(model, "_GAMMA_", gamma);
        return model;
    }

    template <typename data_t>
    static void fill_data_dbgval(data_t *data, size_t size) {
        for (size_t i = 0; i < size; i++) {
            data[i] = static_cast<data_t>(i & (sizeof(data_t) * 8 - 1));
        }
    }
protected:
    virtual void TearDown() {
    }

    virtual void SetUp() {
        try {
            TestsCommon::SetUp();
            math_test_params p = ::testing::WithParamInterface<math_test_params>::GetParam();
            std::string model = getModel(p);

            InferenceEngine::CNNNetReader net_reader;
            ASSERT_NO_THROW(net_reader.ReadNetwork(model.data(), model.length()));

            InferenceEngine::Extension cpuExt(make_so_name("cpu_extension"));
            MKLDNNPlugin::MKLDNNExtensionManager::Ptr extMgr(new MKLDNNPlugin::MKLDNNExtensionManager());
            extMgr->AddExtension(InferenceEngine::IExtensionPtr(&cpuExt, [](InferenceEngine::IExtension*){}));

            MKLDNNGraphTestClass graph;
            graph.CreateGraph(net_reader.getNetwork(), extMgr);

            // Input Data
            InferenceEngine::Blob::Ptr srcData = InferenceEngine::make_shared_blob<float>({ InferenceEngine::Precision::FP32, p.in_out, InferenceEngine::TensorDesc::getLayoutByDims(p.in_out) });
            srcData->allocate();
            if (p.input_tensor.size())
                memcpy(srcData->buffer(), &p.input_tensor[0], sizeof(float)*p.input_tensor.size());
            else {
                if (p.math_function == "Erf")
                    fill_data_sine(srcData->buffer(), srcData->size(), 0.f, 3.f, 1.f);
                else
                    fill_data(srcData->buffer(), srcData->size());
            }
            auto * srcDataPtr = dynamic_cast<InferenceEngine::TBlob<float>*>(srcData.get());
            if (srcDataPtr == nullptr)
                FAIL() << "Cannot cast blob to TBlob<float>.";

            // Output Data
            InferenceEngine::OutputsDataMap out;
            out = net_reader.getNetwork().getOutputsInfo();
            InferenceEngine::BlobMap outputBlobs;

            std::pair<std::string, InferenceEngine::DataPtr> item = *out.begin();

            InferenceEngine::TBlob<float>::Ptr output;
            output = InferenceEngine::make_shared_blob<float>(item.second->getTensorDesc());
            output->allocate();
            outputBlobs[item.first] = output;

            // Output Reference
            InferenceEngine::TBlob<float> dst_ref(item.second->getTensorDesc());
            dst_ref.allocate();

            // Check results
            ref_math(p.math_function, *srcDataPtr, p.alpha, p.beta, p.gamma, dst_ref);
            if (p.reference.size()) {
                for (size_t i = 0; i < p.reference.size(); i++) {
                    ASSERT_NEAR(dst_ref.data()[i], p.reference[i], 0.00001f);
                }
            }

            InferenceEngine::BlobMap srcs;
            srcs.insert(std::pair<std::string, InferenceEngine::Blob::Ptr>("Input", srcData));

            // Infer
            graph.Infer(srcs, outputBlobs);
            float threshold = p.math_function == "Erf" ? 0.0001f : 0.00001f;
            compare(*output, dst_ref, threshold);
        } catch (const InferenceEngine::details::InferenceEngineException &e) {
            FAIL() << e.what();
        }
    }
};

TEST_P(MKLDNNCPUExtMathTests, TestsMath) {}

INSTANTIATE_TEST_CASE_P(
        TestsMath, MKLDNNCPUExtMathTests,
            ::testing::Values(
                // Params: math_function, in_out, input_tensor, alpha, beta, gamma, reference
                math_test_params{ "Erf", {},{},{},{},{},{} },
                math_test_params{ "Erf", { 1, 1, 12, 256 }, {},{},{},{}, {} },
                math_test_params{ "Erf", { 12, 256, 3 },{},{},{},{},{} },
                math_test_params{ "Erf", { 3, 4 },{},{},{},{},{} },
                math_test_params{ "Erf", { 20 },{},{},{},{},{} },
                math_test_params{ "Erf", { 12, 4, 9, 8 },{},{},{},{},{} },
                math_test_params{ "Erf", { 6, 12, 4, 9, 8, 10, 3 },{},{},{},{},{} },
                math_test_params{ "Abs",{ 3 },{ -1, 0, 1 },{},{},{},{ 1, 0, 1 } },
                math_test_params{ "Acos",{ 3 },{ -0.5f, 0.f, 0.5f },{},{},{},{ 2.09439516f, 1.57079637f, 1.04719758f } },
                math_test_params{ "Acosh",{ 3 },{ 1.f, 2.0f, 3.0f },{},{},{},{} },
                math_test_params{ "Asin",{ 3 },{ -0.5f, 0.f, 0.5f },{},{},{},{ -0.523598790f, 0.0f, 0.523598790f } },
                math_test_params{ "Asinh",{ 3 },{ -0.5f, 0.f, 0.5f },{},{},{},{ } },
                math_test_params{ "Atan",{ 3 },{ -1, 0, 1 },{},{},{},{ -0.785398185f, 0.0f, 0.785398185f } },
                math_test_params{ "Atanh",{ 3 },{ -0.5f, 0.f, 0.5f },{},{},{},{ } },
                math_test_params{ "Ceil",{ 2 },{ -1.5f, 1.2f },{},{},{},{ -1, 2 } },
                math_test_params{ "Cos",{ 3 },{ -1, 0, 1 },{},{},{},{ 0.540302336f, 1.0f, 0.540302336f } },
                math_test_params{ "Cosh",{ 3 },{ -0.5f, 0.f, 0.5f },{},{},{},{ } },
                math_test_params{ "Floor",{ 3 },{-1.5f, 1.2f, 2.f},{},{},{},{-2, 1, 2} },
                math_test_params{ "HardSigmoid",{ 3 },{ -1, 0, 1 },{0.5f},{0.6f},{},{ 0.1f, 0.6f, 1.f } },
                math_test_params{ "Log",{ 2 },{ 1, 10 },{},{},{},{ 0.f, 2.30258512f } },
                math_test_params{ "Neg",{ 3 },{ -1, 0, 1 },{},{},{},{ 1, 0, -1 } },
                math_test_params{ "Reciprocal",{ 3 },{ -1, 0.1, 1 },{2},{},{3},{-1, 10, 1} },
                math_test_params{ "Selu",{ 3 },{ -1, 0, 1 },{2},{},{3},{ -3.79272318f, 0.f, 3.f } },
                math_test_params{ "Sign",{ 3 },{ -0.5f, 0.f, 0.5f },{},{},{},{-1, 0, 1} },
                math_test_params{ "Sin",{ 3 },{ -1, 0, 1 },{},{},{},{ -0.841470957f, 0.0f, 0.841470957f } },
                math_test_params{ "Sinh",{ 3 },{ -0.5f, 0.f, 0.5f },{},{},{},{ } },
                math_test_params{ "Softplus",{ 3 },{ -1, 0, 1 },{},{},{},{ 0.31326166f, 0.69314718f, 1.31326163f } },
                math_test_params{ "Softsign",{ 3 },{ -1, 0, 1 },{},{},{},{ -0.5f, 0.f, 0.5f } },
                math_test_params{ "Tan",{ 3 },{ -1, 0, 1 },{},{},{},{ -1.55740774f, 0.0f, 1.55740774f } }
            ));